@@ -3,34 +3,75 @@ const fs = require('fs');
3
3
const vm = require ( 'vm' ) ;
4
4
const { Decimal128, ObjectId } = require ( 'mongodb' ) ; // Import MongoDB data types
5
5
6
- function outputMatchesExampleOutput ( filepath , output ) {
7
- // Read the content of the expected output
6
+ /**
7
+ * Compares the actual output from a MongoDB operation against the expected output stored in a file.
8
+ * Supports both ordered and unordered array comparisons, and performs deep comparisons of nested objects.
9
+ * MongoDB-specific types such as Decimal128, ObjectId, and Date are normalized for consistent comparisons.
10
+ *
11
+ * @param {string } filepath - The relative path to the expected output file. The file must contain valid JSON.
12
+ * @param {Array<Object> } output - The actual output generated by the MongoDB operation, expected to be an array of objects.
13
+ * @param {string } [comparisonType='unordered'] - The type of comparison for arrays:
14
+ * - 'unordered': Arrays are treated as unordered collections, comparing elements regardless of order.
15
+ * - 'ordered': Arrays are compared in a strict order, with elements matched positionally.
16
+ *
17
+ * @returns {boolean } True if the actual output matches the expected output, false otherwise.
18
+ * - Logs detailed mismatch information in the console if the comparison fails.
19
+ * - Returns false if one or both arrays are undefined.
20
+ *
21
+ * @throws {Error } If `comparisonType` is neither 'ordered' nor 'unordered'.
22
+ *
23
+ * @example
24
+ * // Example: Unordered comparison (default)
25
+ * const matches = outputMatchesExampleOutput('examples/tutorial-output.json', actualOutput);
26
+ * console.log(matches); // true if arrays match regardless of order
27
+ *
28
+ * @example
29
+ * // Example: Ordered comparison
30
+ * const matches = outputMatchesExampleOutput(
31
+ * 'examples/tutorial-output.json',
32
+ * actualOutput,
33
+ * 'ordered'
34
+ * );
35
+ * console.log(matches); // true if arrays match exactly with order preserved
36
+ *
37
+ * @description
38
+ * The function reads the expected output from the specified file, preprocesses for formatting
39
+ * issues (e.g., missing arrays, single-quoted strings), and evaluates it safely in a sandbox environment.
40
+ * Nested fields, including arrays and objects, are recursively normalized for deep comparisons.
41
+ * The function logs mismatched outputs for debugging purposes if the comparison fails.
42
+ */
43
+ function outputMatchesExampleOutput (
44
+ filepath ,
45
+ output ,
46
+ comparisonType = 'unordered'
47
+ ) {
48
+ // Accept 'ordered' or 'unordered'
8
49
const filepathString = '../examples/' + filepath ;
9
50
const outputFilePath = path . resolve ( __dirname , filepathString ) ;
10
51
const rawExpectedOutput = fs . readFileSync ( outputFilePath , 'utf8' ) ;
11
52
12
53
const preprocessFileContents = ( contents ) => {
13
- // Detect multiple objects and wrap them into an array
14
54
const wrappedContents = contents
15
55
. trim ( )
16
- . replace ( / } \n { / g, '},\n{' ) // Add commas between objects if they are concatenated
17
- . replace ( / ' ( .* ?) ' / g, '"$1"' ) // Convert single-quoted values to double quotes
18
- . replace ( / ^ \{ / g, '[{' ) // Wrap first object in an array if no array starts
19
- . replace ( / } $ / g, '}]' ) ; // Wrap last object in an array if no array ends
56
+ . replace ( / } \n { / g, '},\n{' ) // Add commas between concatenated objects
57
+ . replace ( / ' ( .* ?) ' / g, '"$1"' ) // Convert single quotes to double quotes
58
+ . replace ( / ^ \{ / g, '[{' ) // Wrap first object in an array if missing
59
+ . replace ( / } $ / g, '}]' ) ; // Wrap last object in an array if missing
20
60
21
- // Ensure keys are quoted properly
22
61
const processed = wrappedContents . replace (
23
- / ( \b [ a - z A - Z _ ] \w * ) \s * : / g, // Match alphanumeric keys (letters, optional underscores) followed by a colon
24
- '"$1":' // Wrap the key in double quotes without touching the colon
62
+ / ( \b [ a - z A - Z _ ] \w * ) \s * : / g,
63
+ '"$1":' // Wrap keys in double quotes
25
64
) ;
26
65
27
- // Quote any unquoted ISO-like date values
28
66
const finalProcessed = processed . replace (
29
- / : \s * ( [ 0 - 9 ] { 4 } - [ 0 - 9 ] { 2 } - [ 0 - 9 ] { 2 } T [ 0 - 9 ] { 2 } : [ 0 - 9 ] { 2 } : [ 0 - 9 ] { 2 } \. ? [ 0 - 9 ] * Z ? ) / g, // Match ISO-like datetime strings
30
- ( match , dateValue ) => `: "${ dateValue . trim ( ) } "` // Wrap the value in double quotes
67
+ / : \s * ( [ 0 - 9 ] { 4 } - [ 0 - 9 ] { 2 } - [ 0 - 9 ] { 2 } T [ 0 - 9 ] { 2 } : [ 0 - 9 ] { 2 } : [ 0 - 9 ] { 2 } \. ? [ 0 - 9 ] * Z ? ) / g,
68
+ ( match , dateValue ) => {
69
+ const normalizedDate = new Date ( dateValue ) . toISOString ( ) ; // Normalize date to UTC
70
+ return `: "${ normalizedDate } "` ;
71
+ }
31
72
) ;
32
73
33
- return finalProcessed ; // Return final sanitized string
74
+ return finalProcessed ;
34
75
} ;
35
76
36
77
const processedExpectedOutput = preprocessFileContents ( rawExpectedOutput ) ;
@@ -43,51 +84,119 @@ function outputMatchesExampleOutput(filepath, output) {
43
84
44
85
let expectedOutputArray ;
45
86
try {
46
- expectedOutputArray = vm . runInNewContext ( processedExpectedOutput , context ) ; // Safely parse expected output
87
+ expectedOutputArray = vm . runInNewContext ( processedExpectedOutput , context ) ;
47
88
} catch ( error ) {
48
89
console . error ( 'Failed to parse expected output:' , error ) ;
49
90
return false ;
50
91
}
51
92
52
- // Directly use the `output` as it is already expected to be a valid array of objects
53
- const actualOutputArray = output ;
54
-
55
- // Helper function to normalize MongoDB data types for comparison
56
93
const normalizeItem = ( item ) => {
57
94
const normalized = { } ;
58
95
for ( const key in item ) {
59
- if ( item [ key ] instanceof Decimal128 || item [ key ] instanceof ObjectId ) {
60
- normalized [ key ] = item [ key ] . toString ( ) ; // Convert Decimal128 and ObjectId to strings
61
- } else if ( item [ key ] instanceof Date ) {
62
- normalized [ key ] = item [ key ] . toISOString ( ) ; // Convert dates to ISO8601 strings for consistent comparison
96
+ const value = item [ key ] ;
97
+
98
+ if ( value instanceof Decimal128 || value instanceof ObjectId ) {
99
+ normalized [ key ] = value . toString ( ) ;
100
+ } else if ( value instanceof Date ) {
101
+ normalized [ key ] = value . toISOString ( ) ;
102
+ } else if (
103
+ typeof value === 'string' &&
104
+ / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } \. ? [ 0 - 9 ] * Z ? $ / . test ( value )
105
+ ) {
106
+ // Parse and re-normalize string-formatted dates to UTC
107
+ const parsedDate = new Date ( value ) ;
108
+ normalized [ key ] = parsedDate . toISOString ( ) ;
109
+ } else if ( Array . isArray ( value ) ) {
110
+ normalized [ key ] = value . map ( ( element ) =>
111
+ typeof element === 'object' && element !== null
112
+ ? normalizeItem ( element )
113
+ : element
114
+ ) ;
115
+ } else if ( typeof value === 'object' && value !== null ) {
116
+ normalized [ key ] = normalizeItem ( value ) ;
63
117
} else {
64
- normalized [ key ] = item [ key ] ; // Keep other values as-is
118
+ normalized [ key ] = value ;
65
119
}
66
120
}
67
121
return normalized ;
68
122
} ;
69
123
70
- if ( actualOutputArray !== undefined && expectedOutputArray !== undefined ) {
71
- // Check that both arrays contain the same elements, regardless of order
124
+ const areObjectsEqual = ( obj1 , obj2 , comparisonType ) => {
125
+ if (
126
+ typeof obj1 !== 'object' ||
127
+ obj1 === null ||
128
+ typeof obj2 !== 'object' ||
129
+ obj2 === null
130
+ ) {
131
+ return obj1 === obj2 ; // Direct comparison for non-object values
132
+ }
133
+
134
+ if ( Array . isArray ( obj1 ) && Array . isArray ( obj2 ) ) {
135
+ if ( comparisonType === 'unordered' ) {
136
+ const normalizedArray1 = obj1 . map ( normalizeItem ) ;
137
+ const normalizedArray2 = obj2 . map ( normalizeItem ) ;
138
+
139
+ return normalizedArray1 . every ( ( item1 ) =>
140
+ normalizedArray2 . some ( ( item2 ) =>
141
+ areObjectsEqual ( item1 , item2 , comparisonType )
142
+ )
143
+ ) ;
144
+ } else if ( comparisonType === 'ordered' ) {
145
+ return obj1 . every ( ( element , index ) =>
146
+ areObjectsEqual ( element , obj2 [ index ] , comparisonType )
147
+ ) ;
148
+ } else {
149
+ throw new Error (
150
+ `Invalid comparisonType: ${ comparisonType } . Use "ordered" or "unordered".`
151
+ ) ;
152
+ }
153
+ }
154
+
155
+ const keys1 = Object . keys ( obj1 ) . sort ( ) ;
156
+ const keys2 = Object . keys ( obj2 ) . sort ( ) ;
157
+
158
+ if ( keys1 . length !== keys2 . length ) {
159
+ return false ; // Different number of keys
160
+ }
161
+
162
+ for ( const key of keys1 ) {
163
+ if ( ! keys2 . includes ( key ) ) {
164
+ return false ; // Mismatched keys
165
+ }
166
+ const val1 = obj1 [ key ] ;
167
+ const val2 = obj2 [ key ] ;
168
+
169
+ if ( ! areObjectsEqual ( val1 , val2 , comparisonType ) ) {
170
+ return false ; // Mismatched values
171
+ }
172
+ }
173
+
174
+ return true ;
175
+ } ;
176
+
177
+ if ( Array . isArray ( output ) && Array . isArray ( expectedOutputArray ) ) {
72
178
const isEqual =
73
- actualOutputArray . length === expectedOutputArray . length &&
179
+ output . length === expectedOutputArray . length &&
74
180
expectedOutputArray . every ( ( expectedItem ) =>
75
- actualOutputArray . some (
76
- ( actualItem ) =>
77
- JSON . stringify ( normalizeItem ( actualItem ) ) ===
78
- JSON . stringify ( normalizeItem ( expectedItem ) )
181
+ output . some ( ( actualItem ) =>
182
+ areObjectsEqual (
183
+ normalizeItem ( actualItem ) ,
184
+ normalizeItem ( expectedItem ) ,
185
+ comparisonType
186
+ )
79
187
)
80
188
) ;
189
+
81
190
if ( ! isEqual ) {
82
191
console . log ( 'Mismatch between actual output and expected output:' , {
83
- actualOutputArray,
84
- expectedOutputArray,
192
+ actualOutputArray : output . map ( normalizeItem ) ,
193
+ expectedOutputArray : expectedOutputArray . map ( normalizeItem ) ,
85
194
} ) ;
86
195
}
87
196
88
197
return isEqual ;
89
198
} else {
90
- console . log ( 'One or both arrays is undefined.' ) ;
199
+ console . error ( 'One or both arrays is undefined.' ) ;
91
200
return false ;
92
201
}
93
202
}
0 commit comments