@voxgig/sdkgen 4.8.3 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/package.json +1 -1
- package/project/.sdk/model/target/zig.aon +9 -0
- package/project/.sdk/src/cmp/zig/Package_zig.ts +15 -11
- package/project/.sdk/src/cmp/zig/fragment/Entity.fragment.zig +3 -3
- package/project/.sdk/src/cmp/zig/fragment/EntityListOp.fragment.zig +3 -3
- package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -3
- package/project/.sdk/src/cmp/zig/utility_zig.ts +50 -1
- package/project/.sdk/tm/c/tests/vendor/omni/json.c +2 -2
- package/project/.sdk/tm/c/tests/vendor/omni/omni.h +2 -2
- package/project/.sdk/tm/c/tests/vendor/omni/runner.c +2 -2
- package/project/.sdk/tm/c/tests/vendor/omni/util.c +2 -2
- package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/json.clj +2 -2
- package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/runner.clj +2 -2
- package/project/.sdk/tm/clojure/test/vendor/omni/voxgig/omni/util.clj +2 -2
- package/project/.sdk/tm/cpp/test/vendor/omni/json.hpp +2 -2
- package/project/.sdk/tm/cpp/test/vendor/omni/omni.hpp +2 -2
- package/project/.sdk/tm/cpp/test/vendor/omni/util.hpp +2 -2
- package/project/.sdk/tm/csharp/test/vendor/omni/Runner.cs +2 -2
- package/project/.sdk/tm/csharp/test/vendor/omni/Util.cs +2 -2
- package/project/.sdk/tm/csharp/utility/struct/Struct.cs +1 -1
- package/project/.sdk/tm/dart/test/vendor/omni/omni.dart +2 -2
- package/project/.sdk/tm/dart/test/vendor/omni/runner.dart +2 -2
- package/project/.sdk/tm/dart/test/vendor/omni/util.dart +2 -2
- package/project/.sdk/tm/elixir/test/vendor/omni/json.ex +2 -2
- package/project/.sdk/tm/elixir/test/vendor/omni/runner.ex +2 -2
- package/project/.sdk/tm/elixir/test/vendor/omni/util.ex +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugin/capability.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/catalog.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/config.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/depend.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/env.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/export.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/graph.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/host.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/order.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/point.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/ref.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/resolve.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/types.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/util.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugin/version.go +1 -1
- package/project/.sdk/tm/go/feature/secrets/plugins/aws/aws.go +4 -4
- package/project/.sdk/tm/go/feature/secrets/plugins/aws/sigv4.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/azuresecrets/azuresecrets.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/boru/boru.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/doppler/doppler.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/gcpsecrets/gcpsecrets.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/hashicorp/hashicorp.go +4 -5
- package/project/.sdk/tm/go/feature/secrets/plugins/httpjson/httpjson.go +3 -3
- package/project/.sdk/tm/go/feature/secrets/plugins/infisical/infisical.go +3 -4
- package/project/.sdk/tm/go/feature/secrets/plugins/onepassword/onepassword.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/plugins/secretspec/secretspec.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/sekreto/addr.go +2 -2
- package/project/.sdk/tm/go/feature/secrets/sekreto/providers.go +5 -2
- package/project/.sdk/tm/go/feature/secrets/sekreto/sekreto.go +31 -2
- package/project/.sdk/tm/go/test/omni/omni.go +8 -3
- package/project/.sdk/tm/go/test/omni/util.go +25 -4
- package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +1 -1
- package/project/.sdk/tm/java/test/vendor/omni/Json.java +2 -2
- package/project/.sdk/tm/java/test/vendor/omni/Runner.java +2 -2
- package/project/.sdk/tm/java/test/vendor/omni/Util.java +2 -2
- package/project/.sdk/tm/java/utility/struct/Struct.java +1 -1
- package/project/.sdk/tm/js/src/utility/StructUtility.js +2 -2
- package/project/.sdk/tm/js/test/vendor/omni/index.js +1 -1
- package/project/.sdk/tm/js/test/vendor/omni/runner.js +18 -3
- package/project/.sdk/tm/js/test/vendor/omni/util.js +25 -8
- package/project/.sdk/tm/kotlin/test/vendor/omni/Json.kt +2 -2
- package/project/.sdk/tm/kotlin/test/vendor/omni/Runner.kt +2 -2
- package/project/.sdk/tm/kotlin/test/vendor/omni/Util.kt +2 -2
- package/project/.sdk/tm/kotlin/utility/struct/Struct.kt +1 -1
- package/project/.sdk/tm/lean/test/vendor/omni/Omni.lean +2 -2
- package/project/.sdk/tm/lua/test/vendor/omni/json.lua +2 -2
- package/project/.sdk/tm/lua/test/vendor/omni/regex.lua +2 -2
- package/project/.sdk/tm/lua/test/vendor/omni/runner.lua +2 -2
- package/project/.sdk/tm/lua/test/vendor/omni/util.lua +2 -2
- package/project/.sdk/tm/lua/utility/struct/regex.lua +1 -1
- package/project/.sdk/tm/lua/utility/struct/struct.lua +1 -1
- package/project/.sdk/tm/ocaml/test/vendor/omni/omni.ml +2 -2
- package/project/.sdk/tm/perl/lib/Voxgig/Struct.pm +1 -1
- package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni/Runner.pm +1 -1
- package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni/Util.pm +1 -1
- package/project/.sdk/tm/perl/t/vendor/omni/Voxgig/Omni.pm +1 -1
- package/project/.sdk/tm/php/test/vendor/omni/Runner.php +2 -2
- package/project/.sdk/tm/php/test/vendor/omni/Util.php +2 -2
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/__init__.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/capability.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/catalog.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/config.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/depend.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/env.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/export.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/graph.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/host.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/order.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/point.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/ref.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/resolve.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/types.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_plugin/version.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/__init__.py +3 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/addr.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/aws.py +3 -3
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/azuresecrets.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/boru.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/doppler.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/gcpsecrets.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/hashicorp.py +3 -4
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/httpjson.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/infisical.py +4 -6
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/onepassword.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/secretspec.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/plugins/sigv4.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/providers.py +1 -1
- package/project/.sdk/tm/py/pkg/feature/secrets/voxgig_sekreto/sekreto.py +21 -1
- package/project/.sdk/tm/py/pkg/utility/voxgig_struct/voxgig_struct.py +1 -1
- package/project/.sdk/tm/py/test/voxgig_omni/__init__.py +1 -1
- package/project/.sdk/tm/py/test/voxgig_omni/runner.py +16 -2
- package/project/.sdk/tm/py/test/voxgig_omni/util.py +27 -12
- package/project/.sdk/tm/rb/test/vendor/omni/runner.rb +2 -2
- package/project/.sdk/tm/rb/test/vendor/omni/util.rb +2 -2
- package/project/.sdk/tm/rb/test/vendor/omni/voxgig_omni.rb +2 -2
- package/project/.sdk/tm/rust/tests/vendor/omni/json.rs +2 -2
- package/project/.sdk/tm/rust/tests/vendor/omni/mod.rs +2 -2
- package/project/.sdk/tm/rust/tests/vendor/omni/regex.rs +2 -2
- package/project/.sdk/tm/rust/tests/vendor/omni/runner.rs +2 -2
- package/project/.sdk/tm/rust/tests/vendor/omni/util.rs +2 -2
- package/project/.sdk/tm/scala/sdktest/vendor/omni/Json.scala +2 -2
- package/project/.sdk/tm/scala/sdktest/vendor/omni/Runner.scala +2 -2
- package/project/.sdk/tm/scala/sdktest/vendor/omni/Util.scala +2 -2
- package/project/.sdk/tm/swift/Tests/vendor/omni/Json.swift +2 -2
- package/project/.sdk/tm/swift/Tests/vendor/omni/Runner.swift +2 -2
- package/project/.sdk/tm/swift/Tests/vendor/omni/Util.swift +2 -2
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Capability.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Catalog.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Config.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Depend.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Env.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Export.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/FeatureHost.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Graph.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Host.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Order.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Point.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Ref.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Resolve.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Types.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/Version.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/plugin/index.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/aws.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/azuresecrets.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/boru.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/doppler.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/gcpsecrets.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/hashicorp.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/httpjson.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/infisical.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/onepassword.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/secretspec.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/plugins/sigv4.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/builtin.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +1 -1
- package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +1 -1
- package/project/.sdk/tm/ts/src/utility/StructUtility.ts +2 -2
- package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +1 -1
- package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +1 -1
- package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
- package/project/.sdk/tm/zig/Makefile +15 -1
- package/project/.sdk/tm/zig/build.zig +23 -5
- package/project/.sdk/tm/zig/core/helpers.zig +52 -11
- package/project/.sdk/tm/zig/core/mem.zig +17 -1
- package/project/.sdk/tm/zig/core/utility.zig +23 -19
- package/project/.sdk/tm/zig/feature/audit.zig +2 -2
- package/project/.sdk/tm/zig/feature/cache.zig +2 -2
- package/project/.sdk/tm/zig/feature/debug.zig +5 -5
- package/project/.sdk/tm/zig/feature/netsim.zig +2 -2
- package/project/.sdk/tm/zig/feature/paging.zig +2 -2
- package/project/.sdk/tm/zig/feature/proxy.zig +15 -8
- package/project/.sdk/tm/zig/feature/streaming.zig +5 -5
- package/project/.sdk/tm/zig/feature/support.zig +3 -3
- package/project/.sdk/tm/zig/feature/telemetry.zig +5 -5
- package/project/.sdk/tm/zig/feature/test.zig +10 -5
- package/project/.sdk/tm/zig/test/fh.zig +10 -10
- package/project/.sdk/tm/zig/test/gotcha_test.zig +5 -5
- package/project/.sdk/tm/zig/test/omniresolver.zig +515 -0
- package/project/.sdk/tm/zig/test/omnismoke_test.zig +284 -0
- package/project/.sdk/tm/zig/test/pipeline_test.zig +8 -8
- package/project/.sdk/tm/zig/test/primary_utility_test.zig +144 -154
- package/project/.sdk/tm/zig/test/struct_corpus.zig +264 -187
- package/project/.sdk/tm/zig/test/vendor/omni/omni.zig +1314 -0
- package/project/.sdk/tm/zig/test/vendor/omni/regex.zig +446 -0
- package/project/.sdk/tm/zig/utility/voxgigstruct/regex.zig +7 -4
- package/project/.sdk/tm/zig/utility/voxgigstruct/struct.zig +356 -157
- package/project/sdkgen-package.json +1 -1
- package/project/.sdk/tm/zig/test/struct_runner.zig +0 -465
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
// VENDORED: @voxgig/omni sdk-20260907-0029-0 (zig/src/omni.zig)
|
|
2
|
+
// Source: https://github.com/voxgig/omni @ 274708cc2d12b21707d975543953f845f8444be0 [tag: sdk-20260907-0029-0]
|
|
3
|
+
// License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
|
|
4
|
+
//! Omni: the shared multi-language test runner (Zig port).
|
|
5
|
+
//!
|
|
6
|
+
//! A test spec is plain JSON. The same spec file drives the same tests in
|
|
7
|
+
//! every language that ships an omni port.
|
|
8
|
+
//!
|
|
9
|
+
//! Port of the canonical TypeScript implementation
|
|
10
|
+
//! (typescript/src/Runner.ts). Behaviour must match, case for case.
|
|
11
|
+
//!
|
|
12
|
+
//! Zig has no exceptions, so a failing check is returned as a message:
|
|
13
|
+
//! `if (pack.runset(group, subject)) |failure| { ... }`. A subject reports
|
|
14
|
+
//! failure the same way, with `SubjectResult.err`.
|
|
15
|
+
//!
|
|
16
|
+
//! Memory: every value the runner allocates comes from the allocator given
|
|
17
|
+
//! to `makeRunner`. Use an arena and free it once at the end.
|
|
18
|
+
|
|
19
|
+
const std = @import("std");
|
|
20
|
+
|
|
21
|
+
pub const regex = @import("regex.zig");
|
|
22
|
+
|
|
23
|
+
/// A JSON value. `null` (the Zig optional) is the absence marker, as
|
|
24
|
+
/// distinct from `Json{ .null = {} }`, which is a JSON null.
|
|
25
|
+
pub const Json = std.json.Value;
|
|
26
|
+
|
|
27
|
+
/// Absence: no value at all.
|
|
28
|
+
pub const Maybe = ?Json;
|
|
29
|
+
|
|
30
|
+
pub const NULLMARK = "__NULL__"; // Value is JSON null.
|
|
31
|
+
pub const UNDEFMARK = "__UNDEF__"; // Value is not present.
|
|
32
|
+
pub const EXISTSMARK = "__EXISTS__"; // Value exists (not undefined).
|
|
33
|
+
|
|
34
|
+
// ---- value helpers ---------------------------------------------------
|
|
35
|
+
|
|
36
|
+
pub fn ismap(val: Maybe) bool {
|
|
37
|
+
return if (val) |entry| .object == entry else false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn islist(val: Maybe) bool {
|
|
41
|
+
return if (val) |entry| .array == entry else false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn isnode(val: Maybe) bool {
|
|
45
|
+
return ismap(val) or islist(val);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn isabsent(val: Maybe) bool {
|
|
49
|
+
return null == val;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn isnull(val: Maybe) bool {
|
|
53
|
+
return if (val) |entry| .null == entry else false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub fn isnone(val: Maybe) bool {
|
|
57
|
+
return isabsent(val) or isnull(val);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn isnum(val: Maybe) bool {
|
|
61
|
+
const entry = val orelse return false;
|
|
62
|
+
return .integer == entry or .float == entry;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn isstr(val: Maybe) bool {
|
|
66
|
+
return if (val) |entry| .string == entry else false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub fn asnum(val: Maybe) ?f64 {
|
|
70
|
+
const entry = val orelse return null;
|
|
71
|
+
return switch (entry) {
|
|
72
|
+
.integer => |num| @floatFromInt(num),
|
|
73
|
+
.float => |num| num,
|
|
74
|
+
else => null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn asstr(val: Maybe) ?[]const u8 {
|
|
79
|
+
const entry = val orelse return null;
|
|
80
|
+
return switch (entry) {
|
|
81
|
+
.string => |text| text,
|
|
82
|
+
else => null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Read a map entry. Returns null (absent) when missing.
|
|
87
|
+
pub fn jget(val: Maybe, key: []const u8) Maybe {
|
|
88
|
+
const entry = val orelse return null;
|
|
89
|
+
return switch (entry) {
|
|
90
|
+
.object => |map| map.get(key),
|
|
91
|
+
else => null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn jhas(val: Maybe, key: []const u8) bool {
|
|
96
|
+
const entry = val orelse return false;
|
|
97
|
+
return switch (entry) {
|
|
98
|
+
.object => |map| map.contains(key),
|
|
99
|
+
else => false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn jnum(val: f64) Json {
|
|
104
|
+
return .{ .float = val };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn jstr(val: []const u8) Json {
|
|
108
|
+
return .{ .string = val };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub fn jbool(val: bool) Json {
|
|
112
|
+
return .{ .bool = val };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub fn jlist(alloc: std.mem.Allocator, entries: []const Json) !Json {
|
|
116
|
+
var out = std.json.Array.init(alloc);
|
|
117
|
+
for (entries) |entry| {
|
|
118
|
+
try out.append(entry);
|
|
119
|
+
}
|
|
120
|
+
return .{ .array = out };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub const Pair = struct { []const u8, Json };
|
|
124
|
+
|
|
125
|
+
pub fn jmap(alloc: std.mem.Allocator, entries: []const Pair) !Json {
|
|
126
|
+
var out: std.json.ObjectMap = .{};
|
|
127
|
+
for (entries) |entry| {
|
|
128
|
+
try out.put(alloc, entry[0], entry[1]);
|
|
129
|
+
}
|
|
130
|
+
return .{ .object = out };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Set a map entry, returning the updated value (no-op for non-maps).
|
|
134
|
+
pub fn jset(alloc: std.mem.Allocator, val: Json, key: []const u8, entry: Json) !Json {
|
|
135
|
+
switch (val) {
|
|
136
|
+
.object => |map| {
|
|
137
|
+
var out = map;
|
|
138
|
+
try out.put(alloc, key, entry);
|
|
139
|
+
return .{ .object = out };
|
|
140
|
+
},
|
|
141
|
+
else => return val,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- util ------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/// Deep copy a JSON value. Containers are rebuilt in the given allocator;
|
|
148
|
+
/// scalars are values already.
|
|
149
|
+
pub fn clone(alloc: std.mem.Allocator, val: Maybe) error{OutOfMemory}!Json {
|
|
150
|
+
const entry = val orelse return Json{ .null = {} };
|
|
151
|
+
|
|
152
|
+
return switch (entry) {
|
|
153
|
+
.array => |list| blk: {
|
|
154
|
+
var out = std.json.Array.init(alloc);
|
|
155
|
+
for (list.items) |item| {
|
|
156
|
+
try out.append(try clone(alloc, item));
|
|
157
|
+
}
|
|
158
|
+
break :blk Json{ .array = out };
|
|
159
|
+
},
|
|
160
|
+
.object => |map| blk: {
|
|
161
|
+
var out: std.json.ObjectMap = .{};
|
|
162
|
+
var it = map.iterator();
|
|
163
|
+
while (it.next()) |field| {
|
|
164
|
+
try out.put(alloc, field.key_ptr.*, try clone(alloc, field.value_ptr.*));
|
|
165
|
+
}
|
|
166
|
+
break :blk Json{ .object = out };
|
|
167
|
+
},
|
|
168
|
+
else => entry,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// The callback of a walk.
|
|
173
|
+
pub const Apply = *const fn (alloc: std.mem.Allocator, val: Json, path: []const []const u8) Json;
|
|
174
|
+
|
|
175
|
+
/// Depth-first walk: children first, then the containing node.
|
|
176
|
+
pub fn walk(
|
|
177
|
+
alloc: std.mem.Allocator,
|
|
178
|
+
val: Json,
|
|
179
|
+
apply: Apply,
|
|
180
|
+
path: []const []const u8,
|
|
181
|
+
) error{OutOfMemory}!Json {
|
|
182
|
+
const next = switch (val) {
|
|
183
|
+
.array => |list| blk: {
|
|
184
|
+
var out = std.json.Array.init(alloc);
|
|
185
|
+
for (list.items, 0..) |item, index| {
|
|
186
|
+
const childpath = try std.mem.concat(alloc, []const u8, &.{
|
|
187
|
+
path,
|
|
188
|
+
&.{try std.fmt.allocPrint(alloc, "{d}", .{index})},
|
|
189
|
+
});
|
|
190
|
+
try out.append(try walk(alloc, item, apply, childpath));
|
|
191
|
+
}
|
|
192
|
+
break :blk Json{ .array = out };
|
|
193
|
+
},
|
|
194
|
+
.object => |map| blk: {
|
|
195
|
+
var out: std.json.ObjectMap = .{};
|
|
196
|
+
var it = map.iterator();
|
|
197
|
+
while (it.next()) |field| {
|
|
198
|
+
const childpath = try std.mem.concat(alloc, []const u8, &.{
|
|
199
|
+
path,
|
|
200
|
+
&.{field.key_ptr.*},
|
|
201
|
+
});
|
|
202
|
+
try out.put(alloc, field.key_ptr.*, try walk(alloc, field.value_ptr.*, apply, childpath));
|
|
203
|
+
}
|
|
204
|
+
break :blk Json{ .object = out };
|
|
205
|
+
},
|
|
206
|
+
else => val,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return apply(alloc, next, path);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Read a value by path. Returns null (absent) when any step is missing.
|
|
213
|
+
pub fn getpath(val: Maybe, path: []const []const u8) Maybe {
|
|
214
|
+
var current = val;
|
|
215
|
+
|
|
216
|
+
for (path) |part| {
|
|
217
|
+
const entry = current orelse return null;
|
|
218
|
+
|
|
219
|
+
switch (entry) {
|
|
220
|
+
.array => |list| {
|
|
221
|
+
const index = std.fmt.parseInt(usize, part, 10) catch return null;
|
|
222
|
+
if (index >= list.items.len) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
current = list.items[index];
|
|
226
|
+
},
|
|
227
|
+
.object => |map| {
|
|
228
|
+
current = map.get(part) orelse return null;
|
|
229
|
+
},
|
|
230
|
+
else => return null,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return current;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Deep structural equality: numbers by value, bools never numbers.
|
|
238
|
+
pub fn deepequal(a: Maybe, b: Maybe) bool {
|
|
239
|
+
if (isabsent(a) or isabsent(b)) {
|
|
240
|
+
return isabsent(a) and isabsent(b);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const x = a.?;
|
|
244
|
+
const y = b.?;
|
|
245
|
+
|
|
246
|
+
if (isnum(a) or isnum(b)) {
|
|
247
|
+
if (!isnum(a) or !isnum(b)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const anum = asnum(a).?;
|
|
251
|
+
const bnum = asnum(b).?;
|
|
252
|
+
|
|
253
|
+
// NaN equals NaN: deepequal is structural, not IEEE (as canonical).
|
|
254
|
+
return anum == bnum or (std.math.isNan(anum) and std.math.isNan(bnum));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return switch (x) {
|
|
258
|
+
.null => .null == y,
|
|
259
|
+
.bool => |flag| .bool == y and flag == y.bool,
|
|
260
|
+
.string => |text| .string == y and std.mem.eql(u8, text, y.string),
|
|
261
|
+
.array => |list| blk: {
|
|
262
|
+
if (.array != y) break :blk false;
|
|
263
|
+
if (list.items.len != y.array.items.len) break :blk false;
|
|
264
|
+
for (list.items, 0..) |entry, index| {
|
|
265
|
+
if (!deepequal(entry, y.array.items[index])) break :blk false;
|
|
266
|
+
}
|
|
267
|
+
break :blk true;
|
|
268
|
+
},
|
|
269
|
+
.object => |map| blk: {
|
|
270
|
+
if (.object != y) break :blk false;
|
|
271
|
+
if (map.count() != y.object.count()) break :blk false;
|
|
272
|
+
var it = map.iterator();
|
|
273
|
+
while (it.next()) |field| {
|
|
274
|
+
const other = y.object.get(field.key_ptr.*) orelse break :blk false;
|
|
275
|
+
if (!deepequal(field.value_ptr.*, other)) break :blk false;
|
|
276
|
+
}
|
|
277
|
+
break :blk true;
|
|
278
|
+
},
|
|
279
|
+
else => false,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Render a number the same way in every port: 5.0 prints as 5.
|
|
284
|
+
pub fn numstr(alloc: std.mem.Allocator, val: f64) ![]const u8 {
|
|
285
|
+
if (std.math.isNan(val) or std.math.isInf(val)) {
|
|
286
|
+
return "null";
|
|
287
|
+
}
|
|
288
|
+
if (val == @trunc(val) and @abs(val) < 9007199254740992.0) {
|
|
289
|
+
return std.fmt.allocPrint(alloc, "{d}", .{@as(i64, @intFromFloat(val))});
|
|
290
|
+
}
|
|
291
|
+
return std.fmt.allocPrint(alloc, "{d}", .{val});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Render a string as a JSON string literal.
|
|
295
|
+
pub fn quote(alloc: std.mem.Allocator, val: []const u8) ![]const u8 {
|
|
296
|
+
var out: std.ArrayList(u8) = .empty;
|
|
297
|
+
try out.append(alloc, '"');
|
|
298
|
+
|
|
299
|
+
for (val) |ch| {
|
|
300
|
+
switch (ch) {
|
|
301
|
+
'"' => try out.appendSlice(alloc, "\\\""),
|
|
302
|
+
'\\' => try out.appendSlice(alloc, "\\\\"),
|
|
303
|
+
'\n' => try out.appendSlice(alloc, "\\n"),
|
|
304
|
+
'\r' => try out.appendSlice(alloc, "\\r"),
|
|
305
|
+
'\t' => try out.appendSlice(alloc, "\\t"),
|
|
306
|
+
else => {
|
|
307
|
+
if (0x20 > ch) {
|
|
308
|
+
const escape = try std.fmt.allocPrint(alloc, "\\u{x:0>4}", .{ch});
|
|
309
|
+
try out.appendSlice(alloc, escape);
|
|
310
|
+
} else {
|
|
311
|
+
try out.append(alloc, ch);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try out.append(alloc, '"');
|
|
318
|
+
return out.items;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Compact JSON text with map keys sorted, identical in every port.
|
|
322
|
+
pub fn jsonstr(alloc: std.mem.Allocator, val: Maybe) error{OutOfMemory}![]const u8 {
|
|
323
|
+
const entry = val orelse return "undefined";
|
|
324
|
+
|
|
325
|
+
return switch (entry) {
|
|
326
|
+
.null => "null",
|
|
327
|
+
.bool => |flag| if (flag) "true" else "false",
|
|
328
|
+
.integer, .float, .number_string => try numstr(alloc, asnum(val).?),
|
|
329
|
+
.string => |text| try quote(alloc, text),
|
|
330
|
+
.array => |list| blk: {
|
|
331
|
+
var out: std.ArrayList(u8) = .empty;
|
|
332
|
+
try out.append(alloc, '[');
|
|
333
|
+
for (list.items, 0..) |item, index| {
|
|
334
|
+
if (0 < index) try out.append(alloc, ',');
|
|
335
|
+
try out.appendSlice(alloc, try jsonstr(alloc, item));
|
|
336
|
+
}
|
|
337
|
+
try out.append(alloc, ']');
|
|
338
|
+
break :blk out.items;
|
|
339
|
+
},
|
|
340
|
+
.object => |map| blk: {
|
|
341
|
+
var keys: std.ArrayList([]const u8) = .empty;
|
|
342
|
+
var it = map.iterator();
|
|
343
|
+
while (it.next()) |field| {
|
|
344
|
+
try keys.append(alloc, field.key_ptr.*);
|
|
345
|
+
}
|
|
346
|
+
std.mem.sort([]const u8, keys.items, {}, lessThanStr);
|
|
347
|
+
|
|
348
|
+
var out: std.ArrayList(u8) = .empty;
|
|
349
|
+
try out.append(alloc, '{');
|
|
350
|
+
for (keys.items, 0..) |key, index| {
|
|
351
|
+
if (0 < index) try out.append(alloc, ',');
|
|
352
|
+
try out.appendSlice(alloc, try quote(alloc, key));
|
|
353
|
+
try out.append(alloc, ':');
|
|
354
|
+
try out.appendSlice(alloc, try jsonstr(alloc, map.get(key).?));
|
|
355
|
+
}
|
|
356
|
+
try out.append(alloc, '}');
|
|
357
|
+
break :blk out.items;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn lessThanStr(_: void, a: []const u8, b: []const u8) bool {
|
|
363
|
+
return .lt == std.mem.order(u8, a, b);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Strings verbatim, everything else as compact JSON.
|
|
367
|
+
pub fn stringify(alloc: std.mem.Allocator, val: Maybe) ![]const u8 {
|
|
368
|
+
if (asstr(val)) |text| {
|
|
369
|
+
return text;
|
|
370
|
+
}
|
|
371
|
+
return jsonstr(alloc, val);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// Render a path as a dotted string.
|
|
375
|
+
pub fn pathify(alloc: std.mem.Allocator, path: []const []const u8) ![]const u8 {
|
|
376
|
+
return std.mem.join(alloc, ".", path);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- runner ----------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/// What a subject returns: a value, or the message of a failure.
|
|
382
|
+
pub const SubjectResult = union(enum) {
|
|
383
|
+
ok: Json,
|
|
384
|
+
err: []const u8,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/// The function under test. Zig has no closures, so a subject is a
|
|
388
|
+
/// function pointer plus its own data.
|
|
389
|
+
pub const Subject = struct {
|
|
390
|
+
call: *const fn (self: *const Subject, args: []const Json) SubjectResult,
|
|
391
|
+
data: ?*const anyopaque = null,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/// What an args-subject returns: the arguments it ended up with, alongside
|
|
395
|
+
/// the value - or the message of a failure.
|
|
396
|
+
pub const SubjectArgsResult = union(enum) {
|
|
397
|
+
ok: struct { args: []const Json, res: Json },
|
|
398
|
+
err: []const u8,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/// A subject that may REPLACE its arguments, which `match.args` can then
|
|
402
|
+
/// assert on. `minor/setpath` is the case in point: eight of its nine entries
|
|
403
|
+
/// assert that the store was rewritten in place, and `merge/integrity` all
|
|
404
|
+
/// six.
|
|
405
|
+
///
|
|
406
|
+
/// A consumer whose value model is not `std.json.Value` has to convert, and a
|
|
407
|
+
/// conversion is a copy: whatever it does to its own nodes is invisible here.
|
|
408
|
+
/// Returning the arguments alongside the result is the only channel there is.
|
|
409
|
+
///
|
|
410
|
+
/// Separate from `Subject` rather than replacing it: a signature change would
|
|
411
|
+
/// break every consumer for a capability most subjects do not need. omni-rust,
|
|
412
|
+
/// -cpp, -ocaml, -elixir, -haskell, -clojure, -scala, -swift and -lean carry
|
|
413
|
+
/// the same pair, for the same reason.
|
|
414
|
+
pub const SubjectArgs = struct {
|
|
415
|
+
call: *const fn (self: *const SubjectArgs, args: []const Json) SubjectArgsResult,
|
|
416
|
+
data: ?*const anyopaque = null,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/// The host of the system under test. Every hook is optional.
|
|
420
|
+
pub const Provider = struct {
|
|
421
|
+
subject: ?*const fn (self: *const Provider, name: []const u8) ?*const Subject = null,
|
|
422
|
+
client: ?*const fn (self: *const Provider, options: Maybe) *const Provider = null,
|
|
423
|
+
contextify: ?*const fn (self: *const Provider, val: Json) Json = null,
|
|
424
|
+
/// Build the `match.err` base from the failure, REPLACING `errify`.
|
|
425
|
+
///
|
|
426
|
+
/// Zig reaches this point with a message and nothing else - an error
|
|
427
|
+
/// value carries no payload - so a library whose errors carry a code
|
|
428
|
+
/// has no other way to put one in the base. The hook receives that
|
|
429
|
+
/// message and returns the base, letting a spec assert
|
|
430
|
+
/// `match: {err: {code: "x"}}` instead of pattern-matching prose.
|
|
431
|
+
errify: ?*const fn (self: *const Provider, alloc: std.mem.Allocator, message: []const u8) anyerror!Json = null,
|
|
432
|
+
data: ?*const anyopaque = null,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/// Run-time options for a set of test entries.
|
|
436
|
+
pub const Flags = struct {
|
|
437
|
+
null_: bool = true,
|
|
438
|
+
name: ?[]const u8 = null,
|
|
439
|
+
|
|
440
|
+
pub fn nonull() Flags {
|
|
441
|
+
return .{ .null_ = false };
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/// The newest spec format version this runner understands. A spec with no
|
|
446
|
+
/// OMNI block is version 0: the original, lenient format, frozen forever.
|
|
447
|
+
/// Version 1 turns on strict entry validation (see checkentry).
|
|
448
|
+
pub const SPECVERSION = 1;
|
|
449
|
+
|
|
450
|
+
/// Capability strings this runner supports beyond the version baseline. A
|
|
451
|
+
/// spec's OMNI.requires list is checked against this: an unknown
|
|
452
|
+
/// capability refuses the spec loudly at load time, instead of a lagging
|
|
453
|
+
/// port silently mis-running it. (Empty today; future format features
|
|
454
|
+
/// mint a string here.)
|
|
455
|
+
pub const CAPABILITIES: []const []const u8 = &.{};
|
|
456
|
+
|
|
457
|
+
/// The complete set of fields an entry may carry. Under version 1 anything
|
|
458
|
+
/// else is an error: an unrecognised key is almost always a typo'd
|
|
459
|
+
/// assertion, and a typo'd assertion is a test that silently stopped
|
|
460
|
+
/// testing.
|
|
461
|
+
const ENTRYFIELDS = [_][]const u8{ "in", "args", "ctx", "out", "err", "match", "client", "id", "doc" };
|
|
462
|
+
|
|
463
|
+
fn isentryfield(key: []const u8) bool {
|
|
464
|
+
for (ENTRYFIELDS) |field| {
|
|
465
|
+
if (std.mem.eql(u8, field, key)) return true;
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Load a spec: a path to a JSON file.
|
|
471
|
+
pub fn loadspec(alloc: std.mem.Allocator, io: std.Io, path: []const u8) !Json {
|
|
472
|
+
const text = std.Io.Dir.cwd().readFileAlloc(io, path, alloc, .unlimited) catch {
|
|
473
|
+
return error.CannotReadSpec;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const parsed = try std.json.parseFromSlice(Json, alloc, text, .{});
|
|
477
|
+
|
|
478
|
+
return parsed.value;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Find `primary.<name>`, then `<name>`, then the whole spec.
|
|
482
|
+
pub fn resolvespec(name: []const u8, alltests: Json) Json {
|
|
483
|
+
if (0 == name.len) {
|
|
484
|
+
return alltests;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (jget(jget(alltests, "primary"), name)) |section| {
|
|
488
|
+
return section;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (jget(alltests, name)) |section| {
|
|
492
|
+
return section;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return alltests;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Read the spec's format version from its optional top-level OMNI block,
|
|
499
|
+
/// and refuse a spec this runner cannot faithfully run: a version newer
|
|
500
|
+
/// than SPECVERSION, or a required capability outside CAPABILITIES. Only a
|
|
501
|
+
/// genuinely absent OMNI key is legacy (version 0) - a present-but-null
|
|
502
|
+
/// block, or a present-but-null requires list, is malformed; that
|
|
503
|
+
/// distinction is why this checks presence (jhas) rather than nullness.
|
|
504
|
+
/// This is a load-time failure, so it surfaces through makeRunner's own
|
|
505
|
+
/// error-union return path rather than the ?[]const u8 channel runset
|
|
506
|
+
/// failures use - Zig error values carry no message payload, so (unlike
|
|
507
|
+
/// the checkset/checkentry failures below) this one is exempt from
|
|
508
|
+
/// carrying byte-identical text; the distinct error name is what a caller
|
|
509
|
+
/// matches on instead.
|
|
510
|
+
fn resolveversion(alltests: Json) !i64 {
|
|
511
|
+
if (!jhas(alltests, "OMNI")) {
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const meta = jget(alltests, "OMNI");
|
|
516
|
+
const version = jget(meta, "version");
|
|
517
|
+
|
|
518
|
+
if (!ismap(meta) or !isnum(version)) {
|
|
519
|
+
return error.MalformedOmniVersion;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const vnum = asnum(version).?;
|
|
523
|
+
if (vnum != @trunc(vnum)) {
|
|
524
|
+
return error.MalformedOmniVersion;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const maxversion: f64 = SPECVERSION;
|
|
528
|
+
if (vnum < 0 or maxversion < vnum) {
|
|
529
|
+
return error.UnsupportedSpecVersion;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (jhas(meta, "requires")) {
|
|
533
|
+
const requires = jget(meta, "requires").?;
|
|
534
|
+
if (!islist(requires)) {
|
|
535
|
+
return error.MalformedOmniRequires;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
for (requires.array.items) |cap| {
|
|
539
|
+
const capstr = asstr(cap) orelse return error.UnsupportedCapability;
|
|
540
|
+
|
|
541
|
+
var known = false;
|
|
542
|
+
for (CAPABILITIES) |allowed| {
|
|
543
|
+
if (std.mem.eql(u8, allowed, capstr)) {
|
|
544
|
+
known = true;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!known) {
|
|
549
|
+
return error.UnsupportedCapability;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return @intFromFloat(vnum);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/// Nulls (and absent values) become NULLMARK. Always a fresh copy.
|
|
558
|
+
pub fn fixjson(alloc: std.mem.Allocator, val: Maybe, donull: bool) error{OutOfMemory}!Json {
|
|
559
|
+
if (isnone(val)) {
|
|
560
|
+
return if (donull) jstr(NULLMARK) else Json{ .null = {} };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const entry = val.?;
|
|
564
|
+
|
|
565
|
+
return switch (entry) {
|
|
566
|
+
.array => |list| blk: {
|
|
567
|
+
var out = std.json.Array.init(alloc);
|
|
568
|
+
for (list.items) |item| {
|
|
569
|
+
try out.append(try fixjson(alloc, item, donull));
|
|
570
|
+
}
|
|
571
|
+
break :blk Json{ .array = out };
|
|
572
|
+
},
|
|
573
|
+
.object => |map| blk: {
|
|
574
|
+
var out: std.json.ObjectMap = .{};
|
|
575
|
+
var it = map.iterator();
|
|
576
|
+
while (it.next()) |field| {
|
|
577
|
+
try out.put(alloc, field.key_ptr.*, try fixjson(alloc, field.value_ptr.*, donull));
|
|
578
|
+
}
|
|
579
|
+
break :blk Json{ .object = out };
|
|
580
|
+
},
|
|
581
|
+
else => entry,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/// The JSON form of an error: always at least {name,message}.
|
|
586
|
+
pub fn errify(alloc: std.mem.Allocator, message: []const u8) !Json {
|
|
587
|
+
return jmap(alloc, &.{ .{ "name", jstr("Error") }, .{ "message", jstr(message) } });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/// Match one leaf: /regex/ or case-insensitive substring for strings.
|
|
591
|
+
pub fn matchval(alloc: std.mem.Allocator, check: Maybe, base: Maybe) !bool {
|
|
592
|
+
if (deepequal(check, base)) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
var want = check;
|
|
597
|
+
if (asstr(check)) |text| {
|
|
598
|
+
if (std.mem.eql(u8, UNDEFMARK, text) or std.mem.eql(u8, NULLMARK, text)) {
|
|
599
|
+
want = Json{ .null = {} };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (isnone(want)) {
|
|
604
|
+
if (isnone(base)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
if (asstr(base)) |text| {
|
|
608
|
+
return std.mem.eql(u8, NULLMARK, text);
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (asstr(want)) |text| {
|
|
614
|
+
// An empty want is not a wildcard: it matches only an empty string.
|
|
615
|
+
if (0 == text.len) {
|
|
616
|
+
return if (asstr(base)) |basetext| 0 == basetext.len else false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const basestr = try stringify(alloc, base);
|
|
620
|
+
|
|
621
|
+
if (2 < text.len and '/' == text[0] and '/' == text[text.len - 1]) {
|
|
622
|
+
return regex.find(alloc, text[1 .. text.len - 1], basestr);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const lowbase = try std.ascii.allocLowerString(alloc, basestr);
|
|
626
|
+
const lowwant = try std.ascii.allocLowerString(alloc, text);
|
|
627
|
+
|
|
628
|
+
return null != std.mem.indexOf(u8, lowbase, lowwant);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return deepequal(want, base);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/// Convert NULLMARK sentinels back into real nulls.
|
|
635
|
+
pub fn nullmodifier(alloc: std.mem.Allocator, val: Json) !Json {
|
|
636
|
+
const text = asstr(val) orelse return val;
|
|
637
|
+
|
|
638
|
+
if (std.mem.eql(u8, NULLMARK, text)) {
|
|
639
|
+
return Json{ .null = {} };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const replaced = try std.mem.replaceOwned(u8, alloc, text, NULLMARK, "null");
|
|
643
|
+
return jstr(replaced);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/// What a runner returns for one named spec section.
|
|
647
|
+
pub const RunPack = struct {
|
|
648
|
+
alloc: std.mem.Allocator,
|
|
649
|
+
spec: Json,
|
|
650
|
+
subject: ?*const Subject,
|
|
651
|
+
client: *const Provider,
|
|
652
|
+
name: []const u8,
|
|
653
|
+
clientnames: []const []const u8,
|
|
654
|
+
clients: []const *const Provider,
|
|
655
|
+
specversion: i64,
|
|
656
|
+
|
|
657
|
+
/// A named group of the resolved spec.
|
|
658
|
+
pub fn set(self: *const RunPack, setname: []const u8) Maybe {
|
|
659
|
+
return jget(self.spec, setname);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/// Run one set of test entries. Returns a failure message, or null.
|
|
663
|
+
pub fn runset(self: *const RunPack, testspec: Maybe, subject: ?*const Subject) !?[]const u8 {
|
|
664
|
+
return self.runsetflags(testspec, .{}, subject);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/// Run one set of test entries with flags.
|
|
668
|
+
pub fn runsetflags(
|
|
669
|
+
self: *const RunPack,
|
|
670
|
+
testspec: Maybe,
|
|
671
|
+
flags: Flags,
|
|
672
|
+
subject: ?*const Subject,
|
|
673
|
+
) !?[]const u8 {
|
|
674
|
+
return self.drive(testspec, flags, subject, null);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/// Everything `runsetflags` does, for a subject that may replace its
|
|
678
|
+
/// arguments. See `SubjectArgs`.
|
|
679
|
+
pub fn runsetflagsargs(
|
|
680
|
+
self: *const RunPack,
|
|
681
|
+
testspec: Maybe,
|
|
682
|
+
flags: Flags,
|
|
683
|
+
argsubject: *const SubjectArgs,
|
|
684
|
+
) !?[]const u8 {
|
|
685
|
+
return self.drive(testspec, flags, null, argsubject);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn drive(
|
|
689
|
+
self: *const RunPack,
|
|
690
|
+
testspec: Maybe,
|
|
691
|
+
flags: Flags,
|
|
692
|
+
subject: ?*const Subject,
|
|
693
|
+
argsubject: ?*const SubjectArgs,
|
|
694
|
+
) !?[]const u8 {
|
|
695
|
+
const alloc = self.alloc;
|
|
696
|
+
const label = flags.name orelse (if (0 == self.name.len) "set" else self.name);
|
|
697
|
+
|
|
698
|
+
const usesubject: ?*const Subject = if (null != argsubject)
|
|
699
|
+
null
|
|
700
|
+
else
|
|
701
|
+
subject orelse self.subject orelse
|
|
702
|
+
return try std.fmt.allocPrint(alloc, "omni: no test subject for: {s}", .{label});
|
|
703
|
+
|
|
704
|
+
const testspecmap = try fixjson(alloc, testspec, flags.null_);
|
|
705
|
+
const testset = jget(testspecmap, "set") orelse
|
|
706
|
+
return try std.fmt.allocPrint(alloc, "omni: test spec has no set: {s}", .{label});
|
|
707
|
+
|
|
708
|
+
if (.array != testset) {
|
|
709
|
+
return try std.fmt.allocPrint(alloc, "omni: test spec has no set: {s}", .{label});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Validate the AUTHORED group (pre-fixjson), not the normalised
|
|
713
|
+
// one: null-normalisation would otherwise rewrite an authored
|
|
714
|
+
// `id: null` into a sentinel string and hide it from checkentry.
|
|
715
|
+
if (1 <= self.specversion) {
|
|
716
|
+
if (try self.checkset(label, testspec, testset)) |failure| {
|
|
717
|
+
return failure;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (testset.array.items, 0..) |rawentry, index| {
|
|
722
|
+
if (.object != rawentry) {
|
|
723
|
+
return try std.fmt.allocPrint(
|
|
724
|
+
alloc,
|
|
725
|
+
"omni: {s}[{d}]: entry is not a map",
|
|
726
|
+
.{ label, index },
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
var entry = rawentry;
|
|
731
|
+
|
|
732
|
+
// An entry with no `out` expects a null (or absent) result.
|
|
733
|
+
if (flags.null_ and isnone(jget(entry, "out"))) {
|
|
734
|
+
entry = try jset(alloc, entry, "out", jstr(NULLMARK));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
var entrysubject: ?*const Subject = usesubject;
|
|
738
|
+
|
|
739
|
+
if (null != entrysubject) if (asstr(jget(entry, "client"))) |clientname| {
|
|
740
|
+
var found: ?*const Provider = null;
|
|
741
|
+
for (self.clientnames, 0..) |candidate, at| {
|
|
742
|
+
if (std.mem.eql(u8, candidate, clientname)) {
|
|
743
|
+
found = self.clients[at];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const client = found orelse
|
|
748
|
+
return try std.fmt.allocPrint(alloc, "omni: unknown client: {s}", .{clientname});
|
|
749
|
+
|
|
750
|
+
if (client.subject) |resolve| {
|
|
751
|
+
if (resolve(client, self.name)) |clientsubject| {
|
|
752
|
+
entrysubject = clientsubject;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Build the argument list: `ctx`, `args`, or `in`.
|
|
758
|
+
var args: std.ArrayList(Json) = .empty;
|
|
759
|
+
const hasctx = jhas(entry, "ctx");
|
|
760
|
+
const hasargs = jhas(entry, "args");
|
|
761
|
+
|
|
762
|
+
if (hasctx) {
|
|
763
|
+
try args.append(alloc, jget(entry, "ctx").?);
|
|
764
|
+
} else if (hasargs) {
|
|
765
|
+
const rawargs = jget(entry, "args").?;
|
|
766
|
+
if (.array == rawargs) {
|
|
767
|
+
for (rawargs.array.items) |item| {
|
|
768
|
+
try args.append(alloc, item);
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
try args.append(alloc, rawargs);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
try args.append(alloc, try clone(alloc, jget(entry, "in")));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if ((hasctx or hasargs) and 0 < args.items.len and .object == args.items[0]) {
|
|
778
|
+
var first = args.items[0];
|
|
779
|
+
if (self.client.contextify) |contextify| {
|
|
780
|
+
first = contextify(self.client, first);
|
|
781
|
+
}
|
|
782
|
+
// Canonical attaches the resolved client onto ctx/args[0]
|
|
783
|
+
// (testpack.client) so a context subject can tell it was
|
|
784
|
+
// invoked through one. std.json.Value has no case for a
|
|
785
|
+
// live Provider reference, and the resolved client is
|
|
786
|
+
// never absent (it defaults to the root provider), so a
|
|
787
|
+
// boolean marker carries the same observable meaning the
|
|
788
|
+
// corpus checks for - presence, never identity.
|
|
789
|
+
first = try jset(alloc, first, "client", jbool(true));
|
|
790
|
+
args.items[0] = first;
|
|
791
|
+
entry = try jset(alloc, entry, "ctx", first);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// An args-subject returns its arguments alongside the result, so
|
|
795
|
+
// `match.args` sees what the subject actually did with them.
|
|
796
|
+
const outcome: SubjectArgsResult = if (argsubject) |call|
|
|
797
|
+
call.call(call, args.items)
|
|
798
|
+
else if (entrysubject) |call| switch (call.call(call, args.items)) {
|
|
799
|
+
.ok => |value| SubjectArgsResult{ .ok = .{ .args = args.items, .res = value } },
|
|
800
|
+
.err => |message| SubjectArgsResult{ .err = message },
|
|
801
|
+
} else return try std.fmt.allocPrint(
|
|
802
|
+
alloc,
|
|
803
|
+
"omni: no test subject for: {s}",
|
|
804
|
+
.{label},
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
switch (outcome) {
|
|
808
|
+
.err => |message| {
|
|
809
|
+
if (try self.handleerror(label, index, entry, message)) |failure| {
|
|
810
|
+
return failure;
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
.ok => |call| {
|
|
814
|
+
const res = try fixjson(alloc, call.res, flags.null_);
|
|
815
|
+
entry = try jset(alloc, entry, "res", res);
|
|
816
|
+
|
|
817
|
+
if (try self.checkresult(label, index, entry, call.args, res)) |failure| {
|
|
818
|
+
return failure;
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Validate a version-1 group up front, against the AUTHORED entries -
|
|
828
|
+
// see the note on runsetflags's call site for why. Absorbs the
|
|
829
|
+
// empty-set check too: an empty `set` is refused unless the group
|
|
830
|
+
// carries `empty: true`, making the emptiness a stated fact rather
|
|
831
|
+
// than an accident.
|
|
832
|
+
fn checkset(self: *const RunPack, label: []const u8, testspec: Maybe, normalset: Json) !?[]const u8 {
|
|
833
|
+
const origset = if (ismap(testspec) and islist(jget(testspec, "set")))
|
|
834
|
+
jget(testspec, "set").?
|
|
835
|
+
else
|
|
836
|
+
normalset;
|
|
837
|
+
|
|
838
|
+
var isempty = false;
|
|
839
|
+
if (ismap(testspec)) {
|
|
840
|
+
if (jget(testspec, "empty")) |flag| {
|
|
841
|
+
isempty = .bool == flag and flag.bool;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (0 == origset.array.items.len and !isempty) {
|
|
846
|
+
return try std.fmt.allocPrint(self.alloc, "omni: empty test set: {s}", .{label});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (origset.array.items, 0..) |entry, index| {
|
|
850
|
+
if (try self.checkentry(label, index, entry)) |failure| {
|
|
851
|
+
return failure;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Strict entry validation, applied when the spec declares version 1
|
|
859
|
+
// or later. The lenient format converts each of these mistakes into a
|
|
860
|
+
// silent pass or a dead field; here they fail with the entry named.
|
|
861
|
+
fn checkentry(self: *const RunPack, label: []const u8, index: usize, entry: Json) !?[]const u8 {
|
|
862
|
+
if (.object != entry) {
|
|
863
|
+
return try self.fail(label, index, entry, "entry is not a map", null, null);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
var fields = entry.object.iterator();
|
|
867
|
+
while (fields.next()) |field| {
|
|
868
|
+
if (!isentryfield(field.key_ptr.*)) {
|
|
869
|
+
const reason = try std.fmt.allocPrint(
|
|
870
|
+
self.alloc,
|
|
871
|
+
"unknown entry field: {s}",
|
|
872
|
+
.{field.key_ptr.*},
|
|
873
|
+
);
|
|
874
|
+
return try self.fail(label, index, entry, reason, null, null);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
var argsources: usize = 0;
|
|
879
|
+
for ([_][]const u8{ "in", "args", "ctx" }) |key| {
|
|
880
|
+
if (jhas(entry, key)) argsources += 1;
|
|
881
|
+
}
|
|
882
|
+
if (1 < argsources) {
|
|
883
|
+
return try self.fail(label, index, entry, "entry has more than one of in, args, ctx", null, null);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (!isnone(jget(entry, "err")) and jhas(entry, "out")) {
|
|
887
|
+
return try self.fail(label, index, entry, "entry has both err and out", null, null);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Presence, not nullness: an authored `id: null` must fail here,
|
|
891
|
+
// before resolveentry's null-normalisation ever gets a chance to
|
|
892
|
+
// turn it into the sentinel string "__NULL__".
|
|
893
|
+
if (jhas(entry, "id") and null == asstr(jget(entry, "id"))) {
|
|
894
|
+
return try self.fail(label, index, entry, "entry id is not a string", null, null);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
fn checkresult(
|
|
901
|
+
self: *const RunPack,
|
|
902
|
+
label: []const u8,
|
|
903
|
+
index: usize,
|
|
904
|
+
entry: Json,
|
|
905
|
+
args: []const Json,
|
|
906
|
+
res: Json,
|
|
907
|
+
) !?[]const u8 {
|
|
908
|
+
const alloc = self.alloc;
|
|
909
|
+
var matched = false;
|
|
910
|
+
|
|
911
|
+
const entryerr = jget(entry, "err");
|
|
912
|
+
if (!isnone(entryerr)) {
|
|
913
|
+
return try self.fail(
|
|
914
|
+
label,
|
|
915
|
+
index,
|
|
916
|
+
entry,
|
|
917
|
+
"expected error did not occur",
|
|
918
|
+
try stringify(alloc, entryerr),
|
|
919
|
+
try stringify(alloc, res),
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const check = jget(entry, "match");
|
|
924
|
+
if (!isnone(check)) {
|
|
925
|
+
const base = try jmap(alloc, &.{
|
|
926
|
+
.{ "in", jget(entry, "in") orelse Json{ .null = {} } },
|
|
927
|
+
.{ "args", try jlist(alloc, args) },
|
|
928
|
+
.{ "out", jget(entry, "res") orelse Json{ .null = {} } },
|
|
929
|
+
.{ "ctx", jget(entry, "ctx") orelse Json{ .null = {} } },
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
if (try self.matchcheck(label, index, entry, check.?, base, &.{})) |failure| {
|
|
933
|
+
return failure;
|
|
934
|
+
}
|
|
935
|
+
matched = true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const out = jget(entry, "out");
|
|
939
|
+
|
|
940
|
+
if (deepequal(res, out)) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// NOTE: a match with no explicit out is a complete check on its own.
|
|
945
|
+
if (matched) {
|
|
946
|
+
if (isnone(out)) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
if (asstr(out)) |text| {
|
|
950
|
+
if (std.mem.eql(u8, NULLMARK, text)) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return try self.fail(
|
|
957
|
+
label,
|
|
958
|
+
index,
|
|
959
|
+
entry,
|
|
960
|
+
"result mismatch",
|
|
961
|
+
try stringify(alloc, out),
|
|
962
|
+
try stringify(alloc, res),
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/// The error base a `match.err` sees: the provider's own, when it has one.
|
|
967
|
+
fn errbase(self: *const RunPack, message: []const u8) !Json {
|
|
968
|
+
if (self.client.errify) |hook| {
|
|
969
|
+
return hook(self.client, self.alloc, message);
|
|
970
|
+
}
|
|
971
|
+
return errify(self.alloc, message);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
fn handleerror(
|
|
975
|
+
self: *const RunPack,
|
|
976
|
+
label: []const u8,
|
|
977
|
+
index: usize,
|
|
978
|
+
entry: Json,
|
|
979
|
+
message: []const u8,
|
|
980
|
+
) !?[]const u8 {
|
|
981
|
+
const alloc = self.alloc;
|
|
982
|
+
const entryerr = jget(entry, "err");
|
|
983
|
+
|
|
984
|
+
if (isnone(entryerr)) {
|
|
985
|
+
return try self.fail(label, index, entry, "unexpected error", null, message);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
var istrue = false;
|
|
989
|
+
if (.bool == entryerr.?) {
|
|
990
|
+
istrue = entryerr.?.bool;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (istrue or try matchval(alloc, entryerr, jstr(message))) {
|
|
994
|
+
const check = jget(entry, "match");
|
|
995
|
+
if (!isnone(check)) {
|
|
996
|
+
const base = try jmap(alloc, &.{
|
|
997
|
+
.{ "in", jget(entry, "in") orelse Json{ .null = {} } },
|
|
998
|
+
.{ "out", jget(entry, "res") orelse Json{ .null = {} } },
|
|
999
|
+
.{ "ctx", jget(entry, "ctx") orelse Json{ .null = {} } },
|
|
1000
|
+
.{ "err", try self.errbase(message) },
|
|
1001
|
+
});
|
|
1002
|
+
return self.matchcheck(label, index, entry, check.?, base, &.{});
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return try self.fail(
|
|
1008
|
+
label,
|
|
1009
|
+
index,
|
|
1010
|
+
entry,
|
|
1011
|
+
"error mismatch",
|
|
1012
|
+
try stringify(alloc, entryerr),
|
|
1013
|
+
message,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Check that every leaf of `check` is present, and matches, in `base`.
|
|
1018
|
+
fn matchcheck(
|
|
1019
|
+
self: *const RunPack,
|
|
1020
|
+
label: []const u8,
|
|
1021
|
+
index: usize,
|
|
1022
|
+
entry: Json,
|
|
1023
|
+
check: Json,
|
|
1024
|
+
base: Json,
|
|
1025
|
+
path: []const []const u8,
|
|
1026
|
+
) error{OutOfMemory}!?[]const u8 {
|
|
1027
|
+
const alloc = self.alloc;
|
|
1028
|
+
|
|
1029
|
+
switch (check) {
|
|
1030
|
+
.array => |list| {
|
|
1031
|
+
for (list.items, 0..) |subcheck, at| {
|
|
1032
|
+
const childpath = try std.mem.concat(alloc, []const u8, &.{
|
|
1033
|
+
path,
|
|
1034
|
+
&.{try std.fmt.allocPrint(alloc, "{d}", .{at})},
|
|
1035
|
+
});
|
|
1036
|
+
if (try self.matchcheck(label, index, entry, subcheck, base, childpath)) |failure| {
|
|
1037
|
+
return failure;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
},
|
|
1042
|
+
.object => |map| {
|
|
1043
|
+
var it = map.iterator();
|
|
1044
|
+
while (it.next()) |field| {
|
|
1045
|
+
const childpath = try std.mem.concat(alloc, []const u8, &.{
|
|
1046
|
+
path,
|
|
1047
|
+
&.{field.key_ptr.*},
|
|
1048
|
+
});
|
|
1049
|
+
if (try self.matchcheck(label, index, entry, field.value_ptr.*, base, childpath)) |failure| {
|
|
1050
|
+
return failure;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
},
|
|
1055
|
+
else => {},
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const baseval = getpath(base, path);
|
|
1059
|
+
|
|
1060
|
+
// The sentinels are tested BEFORE the identity check below.
|
|
1061
|
+
// Otherwise a subject returning the literal string "__UNDEF__"
|
|
1062
|
+
// satisfies an assertion that the key is absent - two mutually
|
|
1063
|
+
// exclusive states passing one check. A sentinel that accepts its
|
|
1064
|
+
// own literal is not a sentinel. (NULLMARK still accepts NULLMARK:
|
|
1065
|
+
// under the default null flag a real null has already been
|
|
1066
|
+
// normalised to it, so the two are genuinely indistinguishable
|
|
1067
|
+
// here - that one needs a raw-value escape, not an ordering
|
|
1068
|
+
// change.)
|
|
1069
|
+
if (asstr(check)) |text| {
|
|
1070
|
+
// Explicitly absent: satisfied only by a genuinely missing
|
|
1071
|
+
// key, never by a present null (the distinction the sentinels
|
|
1072
|
+
// exist to keep).
|
|
1073
|
+
if (std.mem.eql(u8, UNDEFMARK, text)) {
|
|
1074
|
+
if (isabsent(baseval)) {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
return try self.fail(
|
|
1078
|
+
label,
|
|
1079
|
+
index,
|
|
1080
|
+
entry,
|
|
1081
|
+
try std.fmt.allocPrint(alloc, "expected absent at {s}", .{try self.matchat(path)}),
|
|
1082
|
+
"absent",
|
|
1083
|
+
try stringify(alloc, baseval),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
// Explicitly null: satisfied only by a present null.
|
|
1087
|
+
if (std.mem.eql(u8, NULLMARK, text)) {
|
|
1088
|
+
if (isnull(baseval)) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
if (asstr(baseval)) |basetext| {
|
|
1092
|
+
if (std.mem.eql(u8, NULLMARK, basetext)) {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return try self.fail(
|
|
1097
|
+
label,
|
|
1098
|
+
index,
|
|
1099
|
+
entry,
|
|
1100
|
+
try std.fmt.allocPrint(alloc, "expected null at {s}", .{try self.matchat(path)}),
|
|
1101
|
+
"null",
|
|
1102
|
+
try stringify(alloc, baseval),
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
// Explicitly present: any present value, including null.
|
|
1106
|
+
if (std.mem.eql(u8, EXISTSMARK, text)) {
|
|
1107
|
+
if (!isabsent(baseval)) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
return try self.fail(
|
|
1111
|
+
label,
|
|
1112
|
+
index,
|
|
1113
|
+
entry,
|
|
1114
|
+
try std.fmt.allocPrint(alloc, "expected present at {s}", .{try self.matchat(path)}),
|
|
1115
|
+
"present",
|
|
1116
|
+
"absent",
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Identical values match. This sits below the sentinel branches
|
|
1122
|
+
// on purpose - see the note above.
|
|
1123
|
+
if (deepequal(check, baseval)) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// A concrete expectation never matches a missing key - a match
|
|
1128
|
+
// leaf against an absent value must fail, not substring-match
|
|
1129
|
+
// "undefined".
|
|
1130
|
+
if (isabsent(baseval)) {
|
|
1131
|
+
return try self.fail(
|
|
1132
|
+
label,
|
|
1133
|
+
index,
|
|
1134
|
+
entry,
|
|
1135
|
+
try std.fmt.allocPrint(alloc, "match failed at {s}", .{try self.matchat(path)}),
|
|
1136
|
+
try stringify(alloc, check),
|
|
1137
|
+
"absent",
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (try matchval(alloc, check, baseval)) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return try self.matchfail(label, index, entry, check, baseval, path);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// A "match failed" report at one path.
|
|
1149
|
+
fn matchfail(
|
|
1150
|
+
self: *const RunPack,
|
|
1151
|
+
label: []const u8,
|
|
1152
|
+
index: usize,
|
|
1153
|
+
entry: Json,
|
|
1154
|
+
check: Maybe,
|
|
1155
|
+
baseval: Maybe,
|
|
1156
|
+
path: []const []const u8,
|
|
1157
|
+
) error{OutOfMemory}![]const u8 {
|
|
1158
|
+
const alloc = self.alloc;
|
|
1159
|
+
return try self.fail(
|
|
1160
|
+
label,
|
|
1161
|
+
index,
|
|
1162
|
+
entry,
|
|
1163
|
+
try std.fmt.allocPrint(alloc, "match failed at {s}", .{try self.matchat(path)}),
|
|
1164
|
+
try stringify(alloc, check),
|
|
1165
|
+
try stringify(alloc, baseval),
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// The location of one match leaf, for failure messages.
|
|
1170
|
+
fn matchat(self: *const RunPack, path: []const []const u8) error{OutOfMemory}![]const u8 {
|
|
1171
|
+
return if (0 == path.len) "<root>" else try pathify(self.alloc, path);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// The spec-defined part of an entry (drop runner bookkeeping).
|
|
1175
|
+
fn entrysummary(self: *const RunPack, entry: Json) !Json {
|
|
1176
|
+
if (.object != entry) {
|
|
1177
|
+
return entry;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
var out: std.json.ObjectMap = .{};
|
|
1181
|
+
var it = entry.object.iterator();
|
|
1182
|
+
while (it.next()) |field| {
|
|
1183
|
+
const key = field.key_ptr.*;
|
|
1184
|
+
if (!std.mem.eql(u8, "res", key) and !std.mem.eql(u8, "thrown", key) and
|
|
1185
|
+
!std.mem.eql(u8, "ctx", key))
|
|
1186
|
+
{
|
|
1187
|
+
try out.put(self.alloc, key, field.value_ptr.*);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return .{ .object = out };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
fn fail(
|
|
1195
|
+
self: *const RunPack,
|
|
1196
|
+
label: []const u8,
|
|
1197
|
+
index: usize,
|
|
1198
|
+
entry: Json,
|
|
1199
|
+
reason: []const u8,
|
|
1200
|
+
expected: ?[]const u8,
|
|
1201
|
+
actual: ?[]const u8,
|
|
1202
|
+
) error{OutOfMemory}![]const u8 {
|
|
1203
|
+
const alloc = self.alloc;
|
|
1204
|
+
|
|
1205
|
+
const id = jget(entry, "id");
|
|
1206
|
+
const idpart = if (isabsent(id))
|
|
1207
|
+
""
|
|
1208
|
+
else
|
|
1209
|
+
try std.fmt.allocPrint(alloc, " ({s})", .{try stringify(alloc, id)});
|
|
1210
|
+
|
|
1211
|
+
var out: std.ArrayList(u8) = .empty;
|
|
1212
|
+
try out.appendSlice(alloc, try std.fmt.allocPrint(
|
|
1213
|
+
alloc,
|
|
1214
|
+
"omni: {s}[{d}]{s}: {s}",
|
|
1215
|
+
.{ label, index, idpart, reason },
|
|
1216
|
+
));
|
|
1217
|
+
|
|
1218
|
+
if (expected) |text| {
|
|
1219
|
+
try out.appendSlice(alloc, try std.fmt.allocPrint(alloc, "\n expected: {s}", .{text}));
|
|
1220
|
+
}
|
|
1221
|
+
if (actual) |text| {
|
|
1222
|
+
try out.appendSlice(alloc, try std.fmt.allocPrint(alloc, "\n actual: {s}", .{text}));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
try out.appendSlice(alloc, try std.fmt.allocPrint(
|
|
1226
|
+
alloc,
|
|
1227
|
+
"\n entry: {s}",
|
|
1228
|
+
.{try stringify(alloc, try self.entrysummary(entry))},
|
|
1229
|
+
));
|
|
1230
|
+
|
|
1231
|
+
return out.items;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
/// A loaded spec plus its provider.
|
|
1236
|
+
pub const Runner = struct {
|
|
1237
|
+
alloc: std.mem.Allocator,
|
|
1238
|
+
alltests: Json,
|
|
1239
|
+
provider: *const Provider,
|
|
1240
|
+
specversion: i64,
|
|
1241
|
+
|
|
1242
|
+
/// Resolve one named section of the spec.
|
|
1243
|
+
pub fn runner(self: *const Runner, name: []const u8, store: Maybe) !RunPack {
|
|
1244
|
+
_ = store;
|
|
1245
|
+
|
|
1246
|
+
const spec = resolvespec(name, self.alltests);
|
|
1247
|
+
|
|
1248
|
+
var clientnames: std.ArrayList([]const u8) = .empty;
|
|
1249
|
+
var clients: std.ArrayList(*const Provider) = .empty;
|
|
1250
|
+
|
|
1251
|
+
// A spec may define clients that a given test run never references.
|
|
1252
|
+
const defclient = jget(jget(spec, "DEF"), "client");
|
|
1253
|
+
if (ismap(defclient)) {
|
|
1254
|
+
if (self.provider.client) |clientmaker| {
|
|
1255
|
+
var it = defclient.?.object.iterator();
|
|
1256
|
+
while (it.next()) |field| {
|
|
1257
|
+
var copts = jget(jget(field.value_ptr.*, "test"), "options");
|
|
1258
|
+
if (isabsent(copts)) {
|
|
1259
|
+
copts = try jmap(self.alloc, &.{});
|
|
1260
|
+
}
|
|
1261
|
+
try clientnames.append(self.alloc, field.key_ptr.*);
|
|
1262
|
+
try clients.append(self.alloc, clientmaker(self.provider, copts));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
var subject: ?*const Subject = null;
|
|
1268
|
+
if (0 < name.len) {
|
|
1269
|
+
if (self.provider.subject) |resolve| {
|
|
1270
|
+
subject = resolve(self.provider, name);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return .{
|
|
1275
|
+
.alloc = self.alloc,
|
|
1276
|
+
.spec = spec,
|
|
1277
|
+
.subject = subject,
|
|
1278
|
+
.client = self.provider,
|
|
1279
|
+
.name = name,
|
|
1280
|
+
.clientnames = clientnames.items,
|
|
1281
|
+
.clients = clients.items,
|
|
1282
|
+
.specversion = self.specversion,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
/// Make a runner for a spec file path and a provider. Resolves the spec's
|
|
1288
|
+
/// format version immediately, so an unsupported or malformed OMNI block
|
|
1289
|
+
/// fails fast, before any group runs.
|
|
1290
|
+
pub fn makeRunner(
|
|
1291
|
+
alloc: std.mem.Allocator,
|
|
1292
|
+
io: std.Io,
|
|
1293
|
+
path: []const u8,
|
|
1294
|
+
provider: *const Provider,
|
|
1295
|
+
) !Runner {
|
|
1296
|
+
const alltests = try loadspec(alloc, io, path);
|
|
1297
|
+
return .{
|
|
1298
|
+
.alloc = alloc,
|
|
1299
|
+
.alltests = alltests,
|
|
1300
|
+
.provider = provider,
|
|
1301
|
+
.specversion = try resolveversion(alltests),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/// Make a runner for an in-memory spec and a provider. Same fail-fast
|
|
1306
|
+
/// version resolution as makeRunner.
|
|
1307
|
+
pub fn makeRunnerSpec(alloc: std.mem.Allocator, spec: Json, provider: *const Provider) !Runner {
|
|
1308
|
+
return .{
|
|
1309
|
+
.alloc = alloc,
|
|
1310
|
+
.alltests = spec,
|
|
1311
|
+
.provider = provider,
|
|
1312
|
+
.specversion = try resolveversion(spec),
|
|
1313
|
+
};
|
|
1314
|
+
}
|