Skip to content

Commit 8f1f151

Browse files
authored
Add Dataset & Element equality methods, greatly speeding up tests that rely on dataset comparisons. (#280)
This change introduces well-defined equality methods for Dataset, Element, and some other data structures. This greatly speeds up tests that rely on checking equality of datasets (that previously needed reflection). For example, it reduces the total test suite from 1m24s to 10s on GitHub actions (mostly due to one test). These methods may also be of use to library users. However, this does mean that if new fields are added to any of these structs it is important for the Equals method to be updated as well. For now this will be enforced during code review (helped by the fact most of these structs should not fail often), but we should investigate lint rules or some auto-generated reflection based tests that can help catch when this doesn't happen (see #281). This change also makes a change to rely on pointers for []*frame.Frame in the PixelDataInfo.
1 parent 5933371 commit 8f1f151

File tree

11 files changed

+524
-51
lines changed

11 files changed

+524
-51
lines changed

cmd/dicomutil/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,6 @@ func generateImage(fr *frame.Frame, frameIndex int, frameSuffix string, wg *sync
186186
func writePixelDataElement(e *dicom.Element, suffix string) {
187187
imageInfo := e.Value.GetValue().(dicom.PixelDataInfo)
188188
for idx, f := range imageInfo.Frames {
189-
generateImage(&f, idx, suffix, nil)
189+
generateImage(f, idx, suffix, nil)
190190
}
191191
}

dataset.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@ func (d *Dataset) String() string {
190190
return b.String()
191191
}
192192

193+
// Equals returns true if this Dataset equals the provided target Dataset,
194+
// otherwise false.
195+
func (d *Dataset) Equals(target *Dataset) bool {
196+
if target == nil || d == nil {
197+
return d == target
198+
}
199+
for idx, e := range d.Elements {
200+
if !e.Equals(target.Elements[idx]) {
201+
return false
202+
}
203+
}
204+
return true
205+
}
206+
193207
type elementWithLevel struct {
194208
e *Element
195209
// l represents the nesting level of the Element

element.go

Lines changed: 159 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dicom
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -24,6 +25,24 @@ type Element struct {
2425
Value Value `json:"value"`
2526
}
2627

28+
// Equals returns true if this Element equals the provided target Element,
29+
// otherwise false.
30+
func (e *Element) Equals(target *Element) bool {
31+
if target == nil || e == nil {
32+
return e == target
33+
}
34+
if !e.Tag.Equals(target.Tag) ||
35+
e.RawValueRepresentation != target.RawValueRepresentation ||
36+
e.ValueLength != target.ValueLength ||
37+
e.ValueRepresentation != target.ValueRepresentation {
38+
return false
39+
}
40+
if !e.Value.Equals(target.Value) {
41+
return false
42+
}
43+
return true
44+
}
45+
2746
func (e *Element) String() string {
2847
var tagName string
2948
if tagInfo, err := tag.Find(e.Tag); err == nil {
@@ -75,6 +94,8 @@ type Value interface {
7594
GetValue() interface{} // TODO: rename to Get to read cleaner
7695
String() string
7796
MarshalJSON() ([]byte, error)
97+
// Equals returns true if this value equals the input Value.
98+
Equals(Value) bool
7899
}
79100

80101
// NewValue creates a new DICOM value for the supplied data. Likely most useful
@@ -204,6 +225,16 @@ func (b *bytesValue) MarshalJSON() ([]byte, error) {
204225
return json.Marshal(b.value)
205226
}
206227

228+
func (b *bytesValue) Equals(target Value) bool {
229+
if target.ValueType() != Bytes {
230+
return false
231+
}
232+
if !bytes.Equal(b.value, target.GetValue().([]byte)) {
233+
return false
234+
}
235+
return true
236+
}
237+
207238
// stringsValue represents a value of []string.
208239
type stringsValue struct {
209240
value []string
@@ -219,34 +250,81 @@ func (s *stringsValue) MarshalJSON() ([]byte, error) {
219250
return json.Marshal(s.value)
220251
}
221252

253+
func (s *stringsValue) Equals(target Value) bool {
254+
if target.ValueType() != Strings {
255+
return false
256+
}
257+
targetVal := target.GetValue().([]string)
258+
if len(s.value) != len(targetVal) {
259+
return false
260+
}
261+
for idx, val := range s.value {
262+
if val != targetVal[idx] {
263+
return false
264+
}
265+
}
266+
return true
267+
}
268+
222269
// intsValue represents a value of []int.
223270
type intsValue struct {
224271
value []int
225272
}
226273

227-
func (s *intsValue) isElementValue() {}
228-
func (s *intsValue) ValueType() ValueType { return Ints }
229-
func (s *intsValue) GetValue() interface{} { return s.value }
230-
func (s *intsValue) String() string {
231-
return fmt.Sprintf("%v", s.value)
274+
func (i *intsValue) isElementValue() {}
275+
func (i *intsValue) ValueType() ValueType { return Ints }
276+
func (i *intsValue) GetValue() interface{} { return i.value }
277+
func (i *intsValue) String() string {
278+
return fmt.Sprintf("%v", i.value)
232279
}
233-
func (s *intsValue) MarshalJSON() ([]byte, error) {
234-
return json.Marshal(s.value)
280+
func (i *intsValue) MarshalJSON() ([]byte, error) {
281+
return json.Marshal(i.value)
282+
}
283+
284+
func (i *intsValue) Equals(target Value) bool {
285+
if target.ValueType() != Ints {
286+
return false
287+
}
288+
targetVal := target.GetValue().([]int)
289+
if len(i.value) != len(targetVal) {
290+
return false
291+
}
292+
for idx, val := range i.value {
293+
if val != targetVal[idx] {
294+
return false
295+
}
296+
}
297+
return true
235298
}
236299

237300
// floatsValue represents a value of []float64.
238301
type floatsValue struct {
239302
value []float64
240303
}
241304

242-
func (s *floatsValue) isElementValue() {}
243-
func (s *floatsValue) ValueType() ValueType { return Floats }
244-
func (s *floatsValue) GetValue() interface{} { return s.value }
245-
func (s *floatsValue) String() string {
246-
return fmt.Sprintf("%v", s.value)
305+
func (f *floatsValue) isElementValue() {}
306+
func (f *floatsValue) ValueType() ValueType { return Floats }
307+
func (f *floatsValue) GetValue() interface{} { return f.value }
308+
func (f *floatsValue) String() string {
309+
return fmt.Sprintf("%v", f.value)
247310
}
248-
func (s *floatsValue) MarshalJSON() ([]byte, error) {
249-
return json.Marshal(s.value)
311+
func (f *floatsValue) MarshalJSON() ([]byte, error) {
312+
return json.Marshal(f.value)
313+
}
314+
func (f *floatsValue) Equals(target Value) bool {
315+
if target.ValueType() != Floats {
316+
return false
317+
}
318+
targetVal := target.GetValue().([]float64)
319+
if len(f.value) != len(targetVal) {
320+
return false
321+
}
322+
for idx, val := range f.value {
323+
if val != targetVal[idx] {
324+
return false
325+
}
326+
}
327+
return true
250328
}
251329

252330
// SequenceItemValue is a Value that represents a single Sequence Item. Learn
@@ -278,6 +356,22 @@ func (s *SequenceItemValue) MarshalJSON() ([]byte, error) {
278356
return json.Marshal(s.elements)
279357
}
280358

359+
func (s *SequenceItemValue) Equals(target Value) bool {
360+
if target.ValueType() != SequenceItem {
361+
return false
362+
}
363+
targetVal := target.GetValue().([]*Element)
364+
if len(s.elements) != len(targetVal) {
365+
return false
366+
}
367+
for idx, val := range s.elements {
368+
if !val.Equals(targetVal[idx]) {
369+
return false
370+
}
371+
}
372+
return true
373+
}
374+
281375
// sequencesValue represents a set of items in a DICOM sequence.
282376
type sequencesValue struct {
283377
value []*SequenceItemValue
@@ -293,6 +387,21 @@ func (s *sequencesValue) String() string {
293387
func (s *sequencesValue) MarshalJSON() ([]byte, error) {
294388
return json.Marshal(s.value)
295389
}
390+
func (s *sequencesValue) Equals(target Value) bool {
391+
if target.ValueType() != Sequences {
392+
return false
393+
}
394+
targetVal := target.GetValue().([]*SequenceItemValue)
395+
if len(s.value) != len(targetVal) {
396+
return false
397+
}
398+
for idx, val := range s.value {
399+
if !val.Equals(targetVal[idx]) {
400+
return false
401+
}
402+
}
403+
return true
404+
}
296405

297406
// PixelDataInfo is a representation of DICOM PixelData.
298407
type PixelDataInfo struct {
@@ -304,7 +413,7 @@ type PixelDataInfo struct {
304413

305414
// Frames hold the processed PixelData frames (either Native or Encapsulated
306415
// PixelData).
307-
Frames []frame.Frame
416+
Frames []*frame.Frame
308417

309418
// ParseErr indicates if there was an error when reading this Frame from the DICOM.
310419
// If this is set, this means fallback behavior was triggered to blindly write the PixelData bytes to an encapsulated frame.
@@ -329,24 +438,47 @@ type pixelDataValue struct {
329438
PixelDataInfo
330439
}
331440

332-
func (e *pixelDataValue) isElementValue() {}
333-
func (e *pixelDataValue) ValueType() ValueType { return PixelData }
334-
func (e *pixelDataValue) GetValue() interface{} { return e.PixelDataInfo }
335-
func (e *pixelDataValue) String() string {
336-
if len(e.Frames) == 0 {
441+
func (p *pixelDataValue) isElementValue() {}
442+
func (p *pixelDataValue) ValueType() ValueType { return PixelData }
443+
func (p *pixelDataValue) GetValue() interface{} { return p.PixelDataInfo }
444+
func (p *pixelDataValue) String() string {
445+
if len(p.Frames) == 0 {
337446
return "empty pixel data"
338447
}
339-
if e.IsEncapsulated {
340-
return fmt.Sprintf("encapsulated FramesLength=%d Frame[0] size=%d", len(e.Frames), len(e.Frames[0].EncapsulatedData.Data))
448+
if p.IsEncapsulated {
449+
return fmt.Sprintf("encapsulated FramesLength=%d Frame[0] size=%d", len(p.Frames), len(p.Frames[0].EncapsulatedData.Data))
341450
}
342-
if e.ParseErr != nil {
343-
return fmt.Sprintf("parseErr err=%s FramesLength=%d Frame[0] size=%d", e.ParseErr.Error(), len(e.Frames), len(e.Frames[0].EncapsulatedData.Data))
451+
if p.ParseErr != nil {
452+
return fmt.Sprintf("parseErr err=%s FramesLength=%d Frame[0] size=%d", p.ParseErr.Error(), len(p.Frames), len(p.Frames[0].EncapsulatedData.Data))
344453
}
345-
return fmt.Sprintf("FramesLength=%d FrameSize rows=%d cols=%d", len(e.Frames), e.Frames[0].NativeData.Rows, e.Frames[0].NativeData.Cols)
454+
return fmt.Sprintf("FramesLength=%d FrameSize rows=%d cols=%d", len(p.Frames), p.Frames[0].NativeData.Rows, p.Frames[0].NativeData.Cols)
346455
}
347456

348-
func (e *pixelDataValue) MarshalJSON() ([]byte, error) {
349-
return json.Marshal(e.PixelDataInfo)
457+
func (p *pixelDataValue) MarshalJSON() ([]byte, error) {
458+
return json.Marshal(p.PixelDataInfo)
459+
}
460+
func (p *pixelDataValue) Equals(target Value) bool {
461+
if target.ValueType() != PixelData {
462+
return false
463+
}
464+
targetVal := target.GetValue().(PixelDataInfo)
465+
if p.IntentionallySkipped != targetVal.IntentionallySkipped ||
466+
p.IntentionallyUnprocessed != targetVal.IntentionallyUnprocessed ||
467+
p.ParseErr != targetVal.ParseErr ||
468+
p.IsEncapsulated != targetVal.IsEncapsulated ||
469+
!bytes.Equal(p.UnprocessedValueData, targetVal.UnprocessedValueData) {
470+
return false
471+
}
472+
targetFrameVal := target.GetValue().(PixelDataInfo).Frames
473+
if len(p.Frames) != len(targetFrameVal) {
474+
return false
475+
}
476+
for idx, val := range p.Frames {
477+
if !val.Equals(targetFrameVal[idx]) {
478+
return false
479+
}
480+
}
481+
return true
350482
}
351483

352484
// MustGetInts attempts to get an Ints value out of the provided value, and will

0 commit comments

Comments
 (0)