1# Copyright (C) 2023 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Tests for the validate() function."""
15
16load("@bazel_skylib//lib:unittest.bzl", "analysistest")
17load(":schema_validation.scl", "validate")
18
19def _string_comparison_test_impl(ctx):
20    env = analysistest.begin(ctx)
21    if ctx.attr.actual != ctx.attr.expected:
22        analysistest.fail(env, "expected '%s' but got '%s'" % (ctx.attr.expected, ctx.attr.actual))
23    return analysistest.end(env)
24
25_string_comparison_raw_test = analysistest.make(
26    _string_comparison_test_impl,
27    attrs = {
28        "actual": attr.string(),
29        "expected": attr.string(),
30    },
31)
32
33def _string_comparison_test(*, name, actual, expected):
34    _string_comparison_raw_test(
35        name = name,
36        actual = actual,
37        expected = expected,
38        # target_under_test is required but unused
39        target_under_test = "//build/bazel/utils:always_on_config_setting",
40    )
41
42def _test_string_success():
43    test_name = "test_string_success"
44    data = "hello, world"
45    schema = {"type": "string"}
46    message = validate(data, schema, fail_on_error = False)
47    _string_comparison_test(
48        name = test_name,
49        expected = "",
50        actual = message,
51    )
52    return test_name
53
54def _choices_success():
55    test_name = "choices_success"
56    data = "bar"
57    schema = {
58        "type": "string",
59        "choices": [
60            "foo",
61            "bar",
62            "baz",
63        ],
64    }
65    message = validate(data, schema, fail_on_error = False)
66    _string_comparison_test(
67        name = test_name,
68        expected = "",
69        actual = message,
70    )
71    return test_name
72
73def _choices_failure():
74    test_name = "choices_failure"
75    data = "qux"
76    schema = {
77        "type": "string",
78        "choices": [
79            "foo",
80            "bar",
81            "baz",
82        ],
83    }
84    message = validate(data, schema, fail_on_error = False)
85    _string_comparison_test(
86        name = test_name,
87        expected = 'Expected one of ["foo", "bar", "baz"], got qux',
88        actual = message,
89    )
90    return test_name
91
92def _value_success():
93    test_name = "value_success"
94    data = "bar"
95    schema = {
96        "type": "string",
97        "value": "bar",
98    }
99    message = validate(data, schema, fail_on_error = False)
100    _string_comparison_test(
101        name = test_name,
102        expected = "",
103        actual = message,
104    )
105    return test_name
106
107def _value_failure():
108    test_name = "value_failure"
109    data = "qux"
110    schema = {
111        "type": "string",
112        "value": "bar",
113    }
114    message = validate(data, schema, fail_on_error = False)
115    _string_comparison_test(
116        name = test_name,
117        expected = "Expected bar, got qux",
118        actual = message,
119    )
120    return test_name
121
122def _length_success():
123    test_name = "length_success"
124    data = {
125        "a": "foo",
126        "b": "foo",
127        "c": "foo",
128        "d": "foo",
129        "e": "foo",
130        "f": "foo",
131    }
132    schema = {
133        "type": "dict",
134        "required_keys": {
135            "a": {
136                "type": "string",
137                "length": 3,
138            },
139            "b": {
140                "type": "string",
141                "length": "<4",
142            },
143            "c": {
144                "type": "string",
145                "length": "<=4",
146            },
147            "d": {
148                "type": "string",
149                "length": ">2",
150            },
151            "e": {
152                "type": "string",
153                "length": ">=2",
154            },
155            "f": {
156                "type": "string",
157                "length": "=3",
158            },
159        },
160    }
161    message = validate(data, schema, fail_on_error = False)
162    _string_comparison_test(
163        name = test_name,
164        expected = "",
165        actual = message,
166    )
167    return test_name
168
169def _length_failure_1():
170    test_name = "length_failure_1"
171    data = "qux"
172    schema = {
173        "type": "string",
174        "length": 4,
175    }
176    message = validate(data, schema, fail_on_error = False)
177    _string_comparison_test(
178        name = test_name,
179        expected = "Expected length 4, got 3",
180        actual = message,
181    )
182    return test_name
183
184def _length_failure_2():
185    test_name = "length_failure_2"
186    data = "qux"
187    schema = {
188        "type": "string",
189        "length": ">3",
190    }
191    message = validate(data, schema, fail_on_error = False)
192    _string_comparison_test(
193        name = test_name,
194        expected = "Expected length >3, got 3",
195        actual = message,
196    )
197    return test_name
198
199def _test_type_failure():
200    test_name = "test_type_failure"
201    data = 5
202    schema = {"type": "string"}
203    message = validate(data, schema, fail_on_error = False)
204    _string_comparison_test(
205        name = test_name,
206        expected = "Expected string, got int",
207        actual = message,
208    )
209    return test_name
210
211def _test_or_success():
212    test_name = "test_or_success"
213    data = "hello, world"
214    schema = {"or": [
215        {"type": "int"},
216        {"type": "string"},
217    ]}
218    message = validate(data, schema, fail_on_error = False)
219    _string_comparison_test(
220        name = test_name,
221        expected = "",
222        actual = message,
223    )
224    return test_name
225
226def _test_or_failure():
227    test_name = "test_or_failure"
228    data = 3.5
229    schema = {"or": [
230        {"type": "int"},
231        {"type": "string"},
232    ]}
233    message = validate(data, schema, fail_on_error = False)
234    _string_comparison_test(
235        name = test_name,
236        expected = "did not match any schemas in 'or' list, errors:\n  Expected int, got float\n  Expected string, got float",
237        actual = message,
238    )
239    return test_name
240
241def _list_of_strings_success():
242    test_name = "list_of_strings_success"
243    data = ["a", "b"]
244    schema = {
245        "type": "list",
246        "of": {"type": "string"},
247    }
248    message = validate(data, schema, fail_on_error = False)
249    _string_comparison_test(
250        name = test_name,
251        expected = "",
252        actual = message,
253    )
254    return test_name
255
256def _list_of_strings_failure():
257    test_name = "list_of_strings_failure"
258    data = ["a", 5, "b"]
259    schema = {
260        "type": "list",
261        "of": {"type": "string"},
262    }
263    message = validate(data, schema, fail_on_error = False)
264    _string_comparison_test(
265        name = test_name,
266        expected = "Expected string, got int",
267        actual = message,
268    )
269    return test_name
270
271def _tuple_of_strings_success():
272    test_name = "tuple_of_strings_success"
273    data = ("a", "b")
274    schema = {
275        "type": "tuple",
276        "of": {"type": "string"},
277    }
278    message = validate(data, schema, fail_on_error = False)
279    _string_comparison_test(
280        name = test_name,
281        expected = "",
282        actual = message,
283    )
284    return test_name
285
286def _tuple_of_strings_failure():
287    test_name = "tuple_of_strings_failure"
288    data = ("a", 5, "b")
289    schema = {
290        "type": "tuple",
291        "of": {"type": "string"},
292    }
293    message = validate(data, schema, fail_on_error = False)
294    _string_comparison_test(
295        name = test_name,
296        expected = "Expected string, got int",
297        actual = message,
298    )
299    return test_name
300
301def _unique_list_of_strings_success():
302    test_name = "unique_list_of_strings_success"
303    data = ["a", "b"]
304    schema = {
305        "type": "list",
306        "of": {"type": "string"},
307        "unique": True,
308    }
309    message = validate(data, schema, fail_on_error = False)
310    _string_comparison_test(
311        name = test_name,
312        expected = "",
313        actual = message,
314    )
315    return test_name
316
317def _unique_list_of_strings_failure():
318    test_name = "unique_list_of_strings_failure"
319    data = ["a", "b", "a"]
320    schema = {
321        "type": "list",
322        "of": {"type": "string"},
323        "unique": True,
324    }
325    message = validate(data, schema, fail_on_error = False)
326    _string_comparison_test(
327        name = test_name,
328        expected = "Expected all elements to be unique, but saw 'a' twice",
329        actual = message,
330    )
331    return test_name
332
333def _dict_success():
334    test_name = "dict_success"
335    data = {
336        "foo": 5,
337        "bar": "baz",
338        "qux": 3.5,
339    }
340    schema = {
341        "type": "dict",
342        "required_keys": {
343            "foo": {"type": "int"},
344            "bar": {"type": "string"},
345        },
346        "optional_keys": {
347            "qux": {"type": "float"},
348        },
349    }
350    message = validate(data, schema, fail_on_error = False)
351    _string_comparison_test(
352        name = test_name,
353        expected = "",
354        actual = message,
355    )
356    return test_name
357
358def _dict_missing_required_key():
359    test_name = "dict_missing_required_key"
360    data = {
361        "foo": 5,
362    }
363    schema = {
364        "type": "dict",
365        "required_keys": {
366            "foo": {"type": "int"},
367            "bar": {"type": "string"},
368        },
369    }
370    message = validate(data, schema, fail_on_error = False)
371    _string_comparison_test(
372        name = test_name,
373        expected = "required key 'bar' not found",
374        actual = message,
375    )
376    return test_name
377
378def _dict_extra_keys():
379    test_name = "dict_extra_keys"
380    data = {
381        "foo": 5,
382        "bar": "hello",
383        "baz": 3.5,
384    }
385    schema = {
386        "type": "dict",
387        "required_keys": {
388            "foo": {"type": "int"},
389        },
390        "optional_keys": {
391            "bar": {"type": "string"},
392        },
393    }
394    message = validate(data, schema, fail_on_error = False)
395    _string_comparison_test(
396        name = test_name,
397        expected = 'keys ["baz"] not allowed, valid keys: ["foo", "bar"]',
398        actual = message,
399    )
400    return test_name
401
402def _dict_generic_keys_success():
403    test_name = "dict_generic_keys_success"
404    data = {
405        "foo": 5,
406        "bar": "hello",
407    }
408    schema = {
409        "type": "dict",
410        "keys": {"type": "string"},
411        "values": {
412            "or": [
413                {"type": "string"},
414                {"type": "int"},
415            ],
416        },
417    }
418    message = validate(data, schema, fail_on_error = False)
419    _string_comparison_test(
420        name = test_name,
421        expected = "",
422        actual = message,
423    )
424    return test_name
425
426def _dict_generic_keys_failure():
427    test_name = "dict_generic_keys_failure"
428    data = {
429        "foo": 5,
430        "bar": "hello",
431        "baz": 3.5,
432    }
433    schema = {
434        "type": "dict",
435        "keys": {"type": "string"},
436        "values": {
437            "or": [
438                {"type": "string"},
439                {"type": "int"},
440            ],
441        },
442    }
443    message = validate(data, schema, fail_on_error = False)
444    _string_comparison_test(
445        name = test_name,
446        expected = "did not match any schemas in 'or' list, errors:\n  Expected string, got float\n  Expected int, got float",
447        actual = message,
448    )
449    return test_name
450
451def _struct_success():
452    test_name = "struct_success"
453    data = struct(
454        foo = 5,
455        bar = "baz",
456        qux = 3.5,
457    )
458    schema = {
459        "type": "struct",
460        "required_fields": {
461            "foo": {"type": "int"},
462            "bar": {"type": "string"},
463        },
464        "optional_fields": {
465            "qux": {"type": "float"},
466        },
467    }
468    message = validate(data, schema, fail_on_error = False)
469    _string_comparison_test(
470        name = test_name,
471        expected = "",
472        actual = message,
473    )
474    return test_name
475
476def _struct_missing_required_field():
477    test_name = "struct_missing_required_field"
478    data = struct(
479        foo = 5,
480    )
481    schema = {
482        "type": "struct",
483        "required_fields": {
484            "foo": {"type": "int"},
485            "bar": {"type": "string"},
486        },
487    }
488    message = validate(data, schema, fail_on_error = False)
489    _string_comparison_test(
490        name = test_name,
491        expected = "required field 'bar' not found",
492        actual = message,
493    )
494    return test_name
495
496def _struct_extra_fields():
497    test_name = "struct_extra_fields"
498    data = struct(
499        foo = 5,
500        bar = "baz",
501        baz = 3.5,
502    )
503    schema = {
504        "type": "struct",
505        "required_fields": {
506            "foo": {"type": "int"},
507        },
508        "optional_fields": {
509            "bar": {"type": "string"},
510        },
511    }
512    message = validate(data, schema, fail_on_error = False)
513    _string_comparison_test(
514        name = test_name,
515        expected = 'fields ["baz"] not allowed, valid keys: ["foo", "bar"]',
516        actual = message,
517    )
518    return test_name
519
520def schema_validation_test_suite(name):
521    native.test_suite(
522        name = name,
523        tests = [
524            _test_string_success(),
525            _choices_success(),
526            _choices_failure(),
527            _value_success(),
528            _value_failure(),
529            _length_success(),
530            _length_failure_1(),
531            _length_failure_2(),
532            _test_type_failure(),
533            _test_or_success(),
534            _test_or_failure(),
535            _list_of_strings_success(),
536            _list_of_strings_failure(),
537            _tuple_of_strings_success(),
538            _tuple_of_strings_failure(),
539            _unique_list_of_strings_success(),
540            _unique_list_of_strings_failure(),
541            _dict_success(),
542            _dict_missing_required_key(),
543            _dict_extra_keys(),
544            _dict_generic_keys_success(),
545            _dict_generic_keys_failure(),
546            _struct_success(),
547            _struct_missing_required_field(),
548            _struct_extra_fields(),
549        ],
550    )
551