git.fiddlerwoaroof.com
Browse code

Improve test coverage, use Exception objects instead of strings

Ed Langley authored on 09/09/2018 23:20:50
Showing 3 changed files
... ...
@@ -1,8 +1,16 @@
1
+export function NoNextMethodError() {}
2
+NoNextMethodError.prototype = Object.create(Error);
3
+
4
+export function NoApplicableMethodError() {}
5
+NoNextMethodError.prototype = Object.create(Error);
6
+
7
+export function NoPrimaryMethodError() {}
8
+NoPrimaryMethodError.prototype = Object.create(NoApplicableMethodError);
9
+
1 10
 const before_qualifier = Symbol.for('before');
2 11
 const after_qualifier = Symbol.for('after');
3 12
 const around_qualifier = Symbol.for('around');
4 13
 
5
-
6 14
 let genfun_prototype = {
7 15
     name: "(placeholder)",
8 16
     lambda_list: [],
... ...
@@ -61,7 +69,7 @@ let method_prototype = {
61 69
     generic_function: {},
62 70
 };
63 71
 
64
-function StandardMethod(
72
+export function StandardMethod(
65 73
     lambda_list, qualifiers, specializers, body
66 74
 ) {
67 75
     if (! (this instanceof StandardMethod) ) {
... ...
@@ -97,7 +105,7 @@ function apply_generic_function(gf, args) {
97 105
     let applicable_methods =
98 106
         compute_applicable_methods_using_classes(gf, required_portion(args));
99 107
     if (applicable_methods.length === 0) {
100
-        throw new Error(`no applicable methods for gf ${gf.name} with args ${JSON.stringify(args)}`);
108
+        throw new NoApplicableMethodError(`no applicable methods for gf ${gf.name} with args ${JSON.stringify(args)}`);
101 109
     } else {
102 110
         return apply_methods(gf, args, applicable_methods);
103 111
     }
... ...
@@ -107,30 +115,83 @@ function method_more_specific_p(m1, m2, required_classes) {
107 115
     const m1specializers = m1.specializers;
108 116
     const m2specializers = m2.specializers;
109 117
 
118
+    let result = null;
110 119
     for (let [spec1, spec2] of m1specializers.map((el, idx) => [el, m2specializers[idx]])) {
111 120
         if (spec1 !== spec2) {
112
-            return sub_specializer_p(spec1, spec2);
121
+            result = sub_specializer_p(spec1, spec2);
122
+            break;
113 123
         }
114 124
     }
125
+
126
+    return result;
115 127
 }
116 128
 
117
-function sub_specializer_p(c1, c2) {
118
-    return c1.isPrototypeOf(c2);
129
+export function sub_specializer_p(c1, c2) {
130
+    let result = false;
131
+    if (c1 instanceof Specializer) {
132
+        result = c1.super_of(c2);
133
+    } else if (c1.prototype !== undefined && c2.prototype !== undefined) {
134
+        result = Object.isPrototypeOf.call(c1.prototype, c2.prototype);
135
+    }
136
+    return result;
119 137
 }
120 138
 
121 139
 const idS = Symbol.for('id');
122 140
 Object.prototype[idS] = function () { return this };
123 141
 
124
-export function matchesSpecializer(obj, specializer) {
125
-    let result = obj === specializer.prototype;
126
-    let objType = typeof obj;
142
+export function Specializer() {}
143
+Specializer.prototype = {
144
+    matches(obj) { return false; },
145
+    super_of(obj) { return false; },
146
+}
147
+
148
+function isSuperset(superset, subset) {
149
+    return Array.from(subset).every(superset.has.bind(superset));
150
+}
151
+
152
+export function Shape(...keys) {
153
+    if (! (this instanceof Shape) ) {
154
+        return new Shape(...keys);
155
+    }
156
+    this.keys = new Set(keys);
157
+}
158
+Shape.prototype = Object.assign(new Specializer(), {
159
+    matches(obj) {
160
+        return Array.from(this.keys).every(key => obj[key] !== undefined);
161
+    },
162
+    super_of(spec) {
163
+        // this is the super of spec
164
+        //     if this.keys is a subset of spec.keys
165
+        // and if this.keys != spec.keys
127 166
 
128
-    if (!result && objType === 'object') {
129
-        result = Object.isPrototypeOf.call(specializer.prototype, obj);
130
-    } else if (objType === 'number') {
131
-        result = matchesSpecializer(Number.prototype, specializer) || matchesSpecializer(specializer.prototype, Number);
132
-    } else if (objType === 'string') {
133
-        result = matchesSpecializer(String.prototype, specializer) || matchesSpecializer(specializer.prototype, String);
167
+        if (!(spec instanceof Shape)) {
168
+            return false;
169
+        }
170
+
171
+        let this_keys_subset_of_spec_keys = isSuperset(spec.keys, this.keys);
172
+        let not_eq = this.keys.size !== spec.keys.size;
173
+
174
+        return this_keys_subset_of_spec_keys && not_eq;
175
+    }
176
+});
177
+
178
+export function matches_specializer(obj, specializer) {
179
+    let objType = typeof obj;
180
+    let specializer_proto = specializer && specializer.prototype
181
+    let result = obj === specializer_proto;
182
+
183
+    if (obj === null && obj === specializer) {
184
+        result = true;
185
+    } else if (specializer && specializer.prototype !== undefined) {
186
+        if (!result && objType === 'object') {
187
+            result = Object.isPrototypeOf.call(specializer_proto, obj);
188
+        } else if (objType === 'number') {
189
+            result = matches_specializer(Number.prototype, specializer) || matches_specializer(specializer_proto, Number);
190
+        } else if (objType === 'string') {
191
+            result = matches_specializer(String.prototype, specializer) || matches_specializer(specializer_proto, String);
192
+        }
193
+    } else if (specializer instanceof Specializer) {
194
+        result = specializer.matches(obj);
134 195
     }
135 196
 
136 197
     return result;
... ...
@@ -139,17 +200,20 @@ export function matchesSpecializer(obj, specializer) {
139 200
 
140 201
 function compute_applicable_methods_using_classes(gf, required_classes) {
141 202
     const applicable_methods = gf.methods.filter(
142
-        method => method.specializers.every((specializer, idx) => matchesSpecializer(required_classes[idx], specializer))
203
+        method => method.specializers.every((specializer, idx) => matches_specializer(required_classes[idx], specializer))
204
+        
143 205
     );
144 206
 
145 207
     applicable_methods.sort((a,b) => {
208
+        let result = 0;
146 209
         if (method_more_specific_p(a,b)) {
147
-            return 1;
210
+            result = 1;
148 211
         }
149 212
         if (method_more_specific_p(b,a)) {
150
-            return -1;
213
+            result = -1;
151 214
         }
152
-        return 0;
215
+
216
+        return result;
153 217
     })
154 218
 
155 219
     return applicable_methods;
... ...
@@ -173,6 +237,19 @@ function arr_eq(a1, a2) {
173 237
     }
174 238
 }
175 239
 
240
+function set_eq(a1, a2) {
241
+    if (a1.length !== a2.length) {
242
+        return false;
243
+    } else {
244
+        let result = true;
245
+        for (let elem of a1) {
246
+            result = result && a2.has(elem);
247
+            if (!result) break;
248
+        }
249
+        return result;
250
+    }
251
+}
252
+
176 253
 const primary_method_p =
177 254
       method => method instanceof WrappedMethod || method.qualifiers.length === 0;
178 255
 const before_method_p =
... ...
@@ -196,7 +273,7 @@ function apply_methods(gf, args, applicable_methods) {
196 273
     const main_call = Object.defineProperty(
197 274
         function() {
198 275
             if (primaries.length === 0) {
199
-                throw new Error(`No primary method for ${gf.name}`);
276
+                throw new NoPrimaryMethodError(`No primary method for ${gf.name}`);
200 277
             }
201 278
 
202 279
             for (let before of befores) {
... ...
@@ -227,15 +304,16 @@ function apply_method(method, args, next_methods) {
227 304
     const method_context = {
228 305
         call_next_method(...cnm_args) {
229 306
             if (next_methods.length === 0) {
230
-                throw new Error(`no next method for genfun ${method.generic_function.name}`);
307
+                throw new NoNextMethodError(`no next method for genfun ${method.generic_function.name}`);
231 308
             }
232 309
 
233 310
             return method instanceof WrappedMethod
234 311
                 ? method.continuation()
235 312
                 : apply_methods(method.generic_function, cnm_args.length > 0 ? cnm_args : args, next_methods);
236 313
         },
314
+
237 315
         get next_method_p() {
238
-            return next_methods.length === 0
316
+            return next_methods.length !== 0
239 317
         }
240 318
     };
241 319
 
... ...
@@ -1,49 +1,68 @@
1
-import * as uut from './genfuns.js';
1
+import * as uut from './genfuns';
2
+import * as e from './genfuns';
2 3
 
3
-describe('matchesSpecializer', () => {
4
-    function AThing() {};
5
-    const an_instance = new AThing();
6
-    
4
+describe('matches_specializer', () => {
7 5
     test('works in expected cases', () => {
8
-        expect(uut.matchesSpecializer(an_instance, AThing)).toBeTruthy();
9
-        expect(uut.matchesSpecializer(an_instance, String)).toBeFalsy();
10
-        expect(uut.matchesSpecializer(an_instance, Object)).toBeTruthy();
6
+        function AThing() { }
7
+        const an_instance = new AThing();
11 8
 
12
-        expect(uut.matchesSpecializer(new String("foobar"), String)).toBeTruthy();
13
-        expect(uut.matchesSpecializer(new String("foobar"), Object)).toBeTruthy();
9
+        expect(uut.matches_specializer(an_instance, AThing)).toBeTruthy();
10
+        expect(uut.matches_specializer(an_instance, String)).toBeFalsy();
11
+        expect(uut.matches_specializer(an_instance, Object)).toBeTruthy();
14 12
 
15
-        expect(uut.matchesSpecializer(new Number(1), Number)).toBeTruthy();
16
-        expect(uut.matchesSpecializer(new Number(1), Object)).toBeTruthy();
17
-        expect(uut.matchesSpecializer(new Number(1), String)).toBeFalsy();
18
-
19
-        expect(uut.matchesSpecializer([], Array)).toBeTruthy();
20
-        expect(uut.matchesSpecializer([], Object)).toBeTruthy();
21
-        expect(uut.matchesSpecializer([], Number)).toBeFalsy();
13
+        expect(uut.matches_specializer([], Array)).toBeTruthy();
14
+        expect(uut.matches_specializer([], Object)).toBeTruthy();
15
+        expect(uut.matches_specializer([], Number)).toBeFalsy();
22 16
 
23 17
         function Foo() {}
24 18
         Foo.prototype = Object.create(null);
25 19
         const inst = new Foo();
26
-        expect(uut.matchesSpecializer(inst, Foo)).toBeTruthy();
27
-        expect(uut.matchesSpecializer(inst, Object)).toBeFalsy();
20
+        expect(uut.matches_specializer(inst, Foo)).toBeTruthy();
21
+        expect(uut.matches_specializer(inst, Object)).toBeFalsy();
22
+
23
+        expect(uut.matches_specializer({a:1}, uut.Shape('a'))).toBeTruthy();
24
+        expect(uut.matches_specializer({a:1, b:2}, uut.Shape('a'))).toBeTruthy();
25
+        expect(uut.matches_specializer({b:2}, uut.Shape('a'))).toBeFalsy();
26
+
27
+        expect(uut.matches_specializer({a:1, b:2, c:3}, uut.Shape('a', 'b', 'c'))).toBeTruthy();
28
+        expect(uut.matches_specializer({a:1, b:2, c:3, d:4}, uut.Shape('a', 'b', 'c'))).toBeTruthy();
29
+        expect(uut.matches_specializer({a:1, c:3}, uut.Shape('a', 'b', 'c'))).toBeFalsy();
30
+        expect(uut.matches_specializer({c:3}, uut.Shape('a', 'b', 'c'))).toBeFalsy();
31
+        expect(uut.matches_specializer({d:3}, uut.Shape('a', 'b', 'c'))).toBeFalsy();
28 32
     });
29 33
 
30
-    test('works in for primitives', () => {
31
-        expect(uut.matchesSpecializer(1, Number)).toBeTruthy();
32
-        expect(uut.matchesSpecializer(1, Object)).toBeTruthy();
33
-        expect(uut.matchesSpecializer(1, String)).toBeFalsy();
34
+    test('null behavior', () => {
35
+        expect(uut.matches_specializer(null, null)).toBeTruthy();
36
+        expect(uut.matches_specializer(null, Number)).toBeFalsy();
37
+        expect(uut.matches_specializer(null, String)).toBeFalsy();
38
+        expect(uut.matches_specializer(null, Object)).toBeFalsy();
39
+    });
34 40
 
35
-        expect(uut.matchesSpecializer("1", String)).toBeTruthy();
36
-        expect(uut.matchesSpecializer("1", Object)).toBeTruthy();
37
-        expect(uut.matchesSpecializer("1", Number)).toBeFalsy();
41
+    test('undefined (the value) behavior', () => {
42
+        expect(uut.matches_specializer(undefined, undefined)).toBeTruthy();
43
+        expect(uut.matches_specializer(undefined, Number)).toBeFalsy();
44
+        expect(uut.matches_specializer(undefined, String)).toBeFalsy();
45
+        expect(uut.matches_specializer(undefined, Object)).toBeFalsy();
46
+    });
38 47
 
39
-        expect(uut.matchesSpecializer(null, Number)).toBeFalsy();
40
-        expect(uut.matchesSpecializer(null, String)).toBeFalsy();
41
-        expect(uut.matchesSpecializer(null, Object)).toBeFalsy();
48
+    test('works for numbers', () => {
49
+        expect(uut.matches_specializer(new Number(1), Number)).toBeTruthy();
50
+        expect(uut.matches_specializer(new Number(1), Object)).toBeTruthy();
51
+        expect(uut.matches_specializer(new Number(1), String)).toBeFalsy();
42 52
 
43
-        expect(uut.matchesSpecializer(undefined, Number)).toBeFalsy();
44
-        expect(uut.matchesSpecializer(undefined, String)).toBeFalsy();
45
-        expect(uut.matchesSpecializer(undefined, Object)).toBeFalsy();
53
+        expect(uut.matches_specializer(1, Number)).toBeTruthy();
54
+        expect(uut.matches_specializer(1, Object)).toBeTruthy();
55
+        expect(uut.matches_specializer(1, String)).toBeFalsy();
46 56
     });
57
+
58
+    test('handles strings', () => {
59
+        expect(uut.matches_specializer(new String("foobar"), String)).toBeTruthy();
60
+        expect(uut.matches_specializer(new String("foobar"), Object)).toBeTruthy();
61
+
62
+        expect(uut.matches_specializer("1", String)).toBeTruthy();
63
+        expect(uut.matches_specializer("1", Object)).toBeTruthy();
64
+        expect(uut.matches_specializer("1", Number)).toBeFalsy();
65
+    })
47 66
 });
48 67
 
49 68
 describe('defgeneric', () => {
... ...
@@ -54,6 +73,12 @@ describe('defgeneric', () => {
54 73
                 .fn(1,2)
55 74
         ).toEqual(1);
56 75
 
76
+        expect(() => {
77
+            uut.defgeneric("foobar", "a")
78
+                .primary([String], function (a) {})
79
+                .fn({})
80
+        }).toThrow(e.NoApplicableMethodError);
81
+
57 82
         expect(
58 83
             uut.defgeneric("testing1", "a", "b")
59 84
                 .primary([Number, Number], (_, __) => 1)
... ...
@@ -95,6 +120,149 @@ describe('defgeneric', () => {
95 120
         ).toEqual('hi');
96 121
         expect(thirdCounts).toEqual(2);
97 122
 
123
+        expect(
124
+            uut.defgeneric("foobar", "a")
125
+                .primary([Object], function (a) {
126
+                    return 1
127
+                })
128
+                .primary([String], function (a) {
129
+                    return 2
130
+                })
131
+                .fn("foobar"))
132
+            .toEqual(2);
133
+    });
134
+
135
+    test('next-method-p works', () => {
136
+        expect.assertions(3);
137
+
138
+        uut.defgeneric("foobar", "a")
139
+            .primary([Object], function (a) {
140
+                expect(this.next_method_p).toBe(false);
141
+            })
142
+            .fn({});
143
+
144
+        uut.defgeneric("foobar", "a")
145
+            .primary([Object], function (a) {
146
+                expect(this.next_method_p).toBe(false);
147
+            })
148
+            .primary([String], function (a) {
149
+                expect(this.next_method_p).toBe(true);
150
+            })
151
+            .fn("foobar");
152
+
153
+        uut.defgeneric("foobar", "a")
154
+            .primary([Object], function (a) {
155
+                expect(this.next_method_p).toBe(false);
156
+            })
157
+            .primary([String], function (a) {
158
+                expect(this.next_method_p).toBe(true);
159
+            })
160
+            .fn(1);
161
+
162
+    });
163
+
164
+    test('call-next-method works', () => {
165
+        expect(() => {
166
+            uut.defgeneric("foobar", "a")
167
+                .primary([Object], function (a) {
168
+                    this.call_next_method();
169
+                })
170
+                .fn({});
171
+        }).toThrow(e.NoNextMethodError);
172
+
173
+        expect(() => {
174
+            uut.defgeneric("foobar", "a")
175
+                .primary([Object], function (a) {
176
+                    this.call_next_method();
177
+                })
178
+                .primary([String], function (a) {
179
+                    return 1;
180
+                })
181
+                .fn({});
182
+        });
183
+
184
+        expect(
185
+            uut.defgeneric("foobar", "a")
186
+                .primary([Object], function (a) {
187
+                    return 1;
188
+                })
189
+                .primary([String], function (a) {
190
+                    return this.call_next_method();
191
+                })
192
+                .fn("foobar")
193
+        ).toEqual(1);
194
+        
195
+        expect(
196
+            uut.defgeneric("foobar", "a", "b")
197
+                .primary ([String, String], function (a,b) {
198
+                    return `1${this.call_next_method()}`;
199
+                })
200
+                .primary ([Object, String], function (a,b) {
201
+                    return `3${this.call_next_method()}`;
202
+                })
203
+                .primary ([String, Object], function (a,b) {
204
+                    return `2${this.call_next_method()}`;
205
+                })
206
+                .primary ([Object, Object], function (a,b) {
207
+                    return `4`;
208
+                }).fn("a", "b")
209
+        ).toEqual("1234");
210
+    });
211
+});
212
+
213
+describe('custom specializers', () => {
214
+    test('Shape works', () => {
215
+        expect(uut.defgeneric("foobar", "a")
216
+               .primary([uut.Shape('a', 'b')], ({a,b}) => a+b)
217
+               .primary([Object], _ => null)
218
+               .fn({a:1,b:2}))
219
+            .toEqual(3);
220
+
221
+        expect(uut.defgeneric("foobar", "a")
222
+               .primary([uut.Shape('a', 'b')], ({a,b}) => a+b)
223
+               .primary([Object], _ => null)
224
+               .fn({a:1,b:2,c:3}))
225
+            .toEqual(3);
226
+
227
+        expect(uut.defgeneric("foobar", "a")
228
+               .primary([uut.Shape('a', 'b')], ({a,b}) => a+b)
229
+               .primary([Object], _ => null)
230
+               .fn({a:1}))
231
+            .toEqual(null);
232
+    });
233
+
234
+    test('Shape, prototype precedence', () => {
235
+        expect(uut.defgeneric("foobar4", "a")
236
+               .primary([uut.Shape('a')], ({a}) => a)
237
+               .primary([uut.Shape('a', 'b')], ({a,b}) => {return a+b})
238
+               .primary([Object], _ => null).fn({a:1,b:3}))
239
+            .toEqual(4);
240
+
241
+        expect(uut.defgeneric("foobar", "a")
242
+               .primary([uut.Shape('a', 'b')], ({a,b}) => a+b)
243
+               .primary([uut.Shape('b')], ({b}) => b)
244
+               .primary([Object], _ => null)
245
+               .fn({a:1,b:2}))
246
+            .toEqual(3);
247
+
248
+        const Foo = function () {}
249
+        Foo.prototype = {a: true, b: null};
250
+        expect(uut.defgeneric("foobar", "a")
251
+               .primary([uut.Shape('a')], function ({a}) { return 'a'; })
252
+               .primary([uut.Shape('a', 'b', 'c')], function ({a,b,c}) { return `c${this.call_next_method()}`; })
253
+               .primary([uut.Shape('a', 'b')], function ({a,b}) { return `b${this.call_next_method()}`; })
254
+               .primary([Object], _ => null)
255
+               .fn(Object.assign(new Foo(), {c: 3})))
256
+            .toEqual('cba');
257
+    });
258
+});
98 259
 
260
+describe("Shape", () => {
261
+    test('super_of', () => {
262
+        expect(uut.Shape().super_of(uut.Shape("a", "b", "c"))).toBeTruthy();
263
+        expect(uut.Shape('a').super_of(uut.Shape('a', 'b'))).toBeTruthy();
264
+        expect(uut.Shape("a", "b").super_of(uut.Shape("a", "b", "c"))).toBeTruthy();
265
+        expect(uut.Shape("a", "b").super_of(uut.Shape("a", "b"))).toBeFalsy();
266
+        expect(uut.Shape("a", "b").super_of(uut.Shape("a"))).toBeFalsy();
99 267
     });
100 268
 });
... ...
@@ -1,3 +1,5 @@
1
+import * as gf from '../src/genfuns';
2
+
1 3
 function zipWith(fn, ...args) {
2 4
     const minLen = Math.min(...args.map(x => x.length));
3 5
     const res = [];