From bd198515072c947ba6e6681da9c6f5e7f26acd9b Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Fri, 24 Oct 2025 20:06:53 +0200 Subject: [PATCH 01/11] working on mocking support --- .vscode/launch.json | 15 +++++ src/mock.zig | 148 ++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 5 ++ tests/mock_object.zig | 85 ++++++++++++++++++++++++ tests/tests.zig | 1 + 5 files changed, 254 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 src/mock.zig create mode 100644 tests/mock_object.zig diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f5c0048 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Tests", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/zig-out/bin/fs_tests", + "cwd": "${workspaceRoot}", + } + ] +} \ No newline at end of file diff --git a/src/mock.zig b/src/mock.zig new file mode 100644 index 0000000..7f42bee --- /dev/null +++ b/src/mock.zig @@ -0,0 +1,148 @@ +// Copyright (c) 2025 Mateusz Stadnik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const interface = @import("interface.zig"); + +const std = @import("std"); + +fn generateMockMethod(comptime Method: type, comptime name: [:0]const u8) std.builtin.Type.StructField { + @compileLog("Generating mock method: ", name); + return std.builtin.Type.StructField{ + .name = name, + .type = Method, + .default_value_ptr = null, + .is_comptime = false, + .alignment = @alignOf(Method), + }; +} + +fn GenerateMockStruct(comptime InterfaceType: type) type { + comptime var fields: []const std.builtin.Type.StructField = &[_]std.builtin.Type.StructField{}; + inline for (std.meta.fields(InterfaceType)) |d| { + @compileLog("Checking method: ", d.name); + fields = fields ++ &[_]std.builtin.Type.StructField{generateMockMethod(d.type, d.name)}; + } + + return @Type(.{ .@"struct" = .{ + .layout = .auto, + .is_tuple = false, + .fields = fields, + .decls = &.{}, + } }); +} + +pub fn MockInterface(comptime InterfaceType: type) type { + const MockType = GenerateMockStruct(InterfaceType.VTable); + return interface.DeriveFromBase(InterfaceType, MockType); +} + +fn deduce_type(comptime T: type) type { + switch (@typeInfo(T)) { + .pointer => return @typeInfo(T).pointer.child, + else => return T, + } +} + +pub fn MockVirtualCall(self: anytype, comptime method_name: [:0]const u8, args: anytype, return_type: type) return_type { + _ = args; // Suppress unused variable warning + if (!@hasField(deduce_type(@TypeOf(self)), "_mock")) { + @compileError("MockVirtualCall called on non-mock object, please add ._mock: interface.GenerateMockTable() to the derived mock struct"); + } + + var expectations = self._mock.expectations.get(method_name) orelse { + std.debug.print("No expectations found for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); + unreachable; + }; + + if (expectations.len() == 0) { + std.debug.print("No more expectations left for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); + unreachable; + } + + const list_node = expectations.popFirst() orelse unreachable; + var expectation = @as(*Expectation, @fieldParentPtr("_node", list_node)); + defer self._mock.allocator.destroy(expectation); + if (expectation._return) |r| { + defer self._mock.allocator.destroy(@as(*return_type, @ptrCast(@alignCast(r)))); + return expectation.getReturnValue(return_type); + } else { + std.debug.print("No return value set for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); + unreachable; + } +} + +pub fn MockDestructorCall(self: anytype) void { + if (!@hasField(deduce_type(@TypeOf(self)), "_mock")) { + @compileError("MockVirtualCall called on non-mock object, please add ._mock: interface.GenerateMockTable() to the derived mock struct"); + } + self._mock.expectations.deinit(); +} + +const Expectation = struct { + _allocator: std.mem.Allocator, + _return: ?*anyopaque, + _node: std.DoublyLinkedList.Node, + + pub fn willReturn(self: *Expectation, value: anytype) void { + const return_object = self._allocator.create(@TypeOf(value)) catch unreachable; + return_object.* = value; + self._return = return_object; + } + + pub fn getReturnValue(self: *Expectation, return_type: type) return_type { + std.testing.expect(self._return != null) catch unreachable; + return @as(*return_type, @ptrCast(@alignCast(self._return.?))).*; + } +}; + +pub const MockTableType = struct { + allocator: std.mem.Allocator, + expectations: std.StringHashMap(std.DoublyLinkedList), + + pub fn init(allocator: std.mem.Allocator) MockTableType { + return MockTableType{ + .allocator = allocator, + .expectations = std.StringHashMap(std.DoublyLinkedList).init(allocator), + }; + } + + pub fn expectCall(self: *MockTableType, comptime method_name: [:0]const u8) *Expectation { + var list = self.expectations.getPtr(method_name) orelse blk: { + self.expectations.put(method_name, std.DoublyLinkedList{}) catch unreachable; + break :blk self.expectations.getPtr(method_name) orelse unreachable; + }; + const expectation = self.allocator.create(Expectation) catch unreachable; + expectation.* = Expectation{ + ._allocator = self.allocator, + ._return = null, + ._node = std.DoublyLinkedList.Node{}, + }; + _ = list.append(&expectation._node); + return expectation; + } +}; + +pub fn GenerateMockTable(InterfaceType: type, allocator: std.mem.Allocator) *MockTableType { + var table = allocator.create(MockTableType) catch unreachable; + table.* = MockTableType.init(allocator); + inline for (std.meta.fields(InterfaceType.VTable)) |field| { + table.expectations.put(field.name, std.DoublyLinkedList{}) catch unreachable; + } + return table; +} diff --git a/src/root.zig b/src/root.zig index dab9154..1612866 100644 --- a/src/root.zig +++ b/src/root.zig @@ -27,3 +27,8 @@ pub const CountingInterfaceVirtualCall = @import("interface.zig").CountingInterf pub const CountingInterfaceDestructorCall = @import("interface.zig").CountingInterfaceDestructorCall; pub const base = @import("interface.zig").GetBase; + +pub const MockVirtualCall = @import("mock.zig").MockVirtualCall; +pub const MockDestructorCall = @import("mock.zig").MockDestructorCall; +pub const GenerateMockTable = @import("mock.zig").GenerateMockTable; +pub const MockTableType = @import("mock.zig").MockTableType; diff --git a/tests/mock_object.zig b/tests/mock_object.zig new file mode 100644 index 0000000..ff616ce --- /dev/null +++ b/tests/mock_object.zig @@ -0,0 +1,85 @@ +// Copyright (c) 2025 Mateusz Stadnik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const std = @import("std"); + +const interface = @import("interface"); + +// Constructs an interface for Shape objects +// all methods are pure virtual, so they must be implemented in the derived types +// SelfType is type of InterfaceHolder struct (in c++ it would be a class with pure virtual methods) +const IShape = interface.ConstructCountingInterface(struct { + pub const Self = @This(); + + pub fn area(self: *const Self) u32 { + return interface.CountingInterfaceVirtualCall(self, "area", .{}, u32); + } + + pub fn set_size(self: *Self, new_size: u32) void { + return interface.CountingInterfaceVirtualCall(self, "set_size", .{new_size}, void); + } + + pub fn allocate_some(self: *Self) void { + return interface.CountingInterfaceVirtualCall(self, "allocate_some", .{}, void); + } + + // do not forget about virtual destructor + pub fn delete(self: *Self) void { + interface.CountingInterfaceDestructorCall(self); + } +}); + +const MockShape = interface.DeriveFromBase(IShape, struct { + const Self = @This(); + _mock: *interface.MockTableType, + + pub fn create() MockShape { + return MockShape.init(.{ + ._mock = interface.GenerateMockTable(IShape, std.testing.allocator), + }); + } + + pub fn area(self: *const Self) u32 { + return interface.MockVirtualCall(self, "area", .{}, u32); + } + + pub fn set_size(self: *Self, new_size: u32) void { + return interface.MockVirtualCall(self, "set_size", .{new_size}, void); + } + + pub fn allocate_some(self: *Self) void { + return interface.MockVirtualCall(self, "allocate_some", .{}, void); + } + + pub fn delete(self: *Self) void { + interface.MockDestructorCall(self); + } +}); + +test "interface can be mocked for tests" { + var mock = MockShape.InstanceType.create(); + var obj = try mock.interface.new(std.testing.allocator); + defer obj.interface.delete(); + + mock.data()._mock + .expectCall("area") + .willReturn(@as(i32, 10)); + + try std.testing.expectEqual(10, obj.interface.area()); +} diff --git a/tests/tests.zig b/tests/tests.zig index a79fde8..b253a76 100644 --- a/tests/tests.zig +++ b/tests/tests.zig @@ -23,4 +23,5 @@ comptime { _ = @import("shared.zig"); _ = @import("duplicate.zig"); _ = @import("duplicate_counting.zig"); + _ = @import("mock_object.zig"); } From a46f4d6a4a440dab7434e2db1941474923409512 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 09:44:03 +0200 Subject: [PATCH 02/11] wip --- src/mock.zig | 110 ++++++++++++++++++++++++++++++++++++------ tests/mock_object.zig | 23 +++++++-- 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/mock.zig b/src/mock.zig index 7f42bee..bb446e3 100644 --- a/src/mock.zig +++ b/src/mock.zig @@ -61,11 +61,11 @@ fn deduce_type(comptime T: type) type { pub fn MockVirtualCall(self: anytype, comptime method_name: [:0]const u8, args: anytype, return_type: type) return_type { _ = args; // Suppress unused variable warning - if (!@hasField(deduce_type(@TypeOf(self)), "_mock")) { - @compileError("MockVirtualCall called on non-mock object, please add ._mock: interface.GenerateMockTable() to the derived mock struct"); + if (!@hasField(deduce_type(@TypeOf(self)), "mock")) { + @compileError("MockVirtualCall called on non-mock object, please add .mock: interface.GenerateMockTable() to the derived mock struct"); } - var expectations = self._mock.expectations.get(method_name) orelse { + var expectations = self.mock.expectations.getPtr(method_name) orelse { std.debug.print("No expectations found for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); unreachable; }; @@ -75,40 +75,115 @@ pub fn MockVirtualCall(self: anytype, comptime method_name: [:0]const u8, args: unreachable; } - const list_node = expectations.popFirst() orelse unreachable; - var expectation = @as(*Expectation, @fieldParentPtr("_node", list_node)); - defer self._mock.allocator.destroy(expectation); - if (expectation._return) |r| { - defer self._mock.allocator.destroy(@as(*return_type, @ptrCast(@alignCast(r)))); - return expectation.getReturnValue(return_type); - } else { - std.debug.print("No return value set for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); - unreachable; + const list_node = expectations.first orelse unreachable; + const expectation = @as(*Expectation, @fieldParentPtr("_node", list_node)); + expectation._times -= 1; + const ret = expectation.getReturnValue(return_type); + if (expectation._times == 0) { + defer self.mock.allocator.destroy(expectation); + if (expectation._return) |r| { + defer { + self.mock.allocator.destroy(@as(*return_type, @ptrCast(@alignCast(r)))); + expectation._return = null; + } + } + expectations.remove(list_node); } + return ret; } pub fn MockDestructorCall(self: anytype) void { - if (!@hasField(deduce_type(@TypeOf(self)), "_mock")) { - @compileError("MockVirtualCall called on non-mock object, please add ._mock: interface.GenerateMockTable() to the derived mock struct"); + if (!@hasField(deduce_type(@TypeOf(self)), "mock")) { + @compileError("MockVirtualCall called on non-mock object, please add .mock: interface.GenerateMockTable() to the derived mock struct"); + } + var it = self.mock.expectations.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.len() != 0) { + std.debug.print("Not all expectations were met for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), entry.key_ptr.* }); + } + var list = entry.value_ptr; + while (list.pop()) |node| { + var expectation = @as(*Expectation, @fieldParentPtr("_node", node)); + expectation.delete(); + } } - self._mock.expectations.deinit(); + self.mock.expectations.deinit(); + self.mock.allocator.destroy(self.mock); +} + +const ArgMatcher = struct { + alignment: u8, + value_ptr: *anyopaque, + match: *const fn (*anyopaque, *anyopaque) bool, + deinit: *const fn (*anyopaque) void, +}; + +const ArgsMatcher = struct { + allocator: std.mem.Allocator, + args: std.ArrayList(ArgMatcher), + + pub fn deinit(self: *ArgsMatcher) void { + self.args.deinit(self.allocator); + } +}; + +fn isTuple(comptime T: type) bool { + const info = @typeInfo(T); + return switch (info) { + .@"struct" => info.@"struct".is_tuple, + else => false, + }; } const Expectation = struct { _allocator: std.mem.Allocator, _return: ?*anyopaque, _node: std.DoublyLinkedList.Node, + _deinit: ?*const fn (self: *Expectation) void, + _args_matcher: ArgsMatcher, + _times: i32, - pub fn willReturn(self: *Expectation, value: anytype) void { + pub fn willReturn(self: *Expectation, value: anytype) *Expectation { const return_object = self._allocator.create(@TypeOf(value)) catch unreachable; return_object.* = value; self._return = return_object; + const FunctorType = struct { + pub fn call(s: *Expectation) void { + if (s._return) |r| { + s._allocator.destroy(@as(*@TypeOf(value), @ptrCast(@alignCast(r)))); + s._return = null; + } + } + }; + self._deinit = &FunctorType.call; + return self; + } + + pub fn times(self: *Expectation, count: i32) *Expectation { + self._times = count; + return self; } pub fn getReturnValue(self: *Expectation, return_type: type) return_type { + if (return_type == void) { + return; + } std.testing.expect(self._return != null) catch unreachable; return @as(*return_type, @ptrCast(@alignCast(self._return.?))).*; } + + pub fn withArgs(self: *Expectation, args: anytype) *Expectation { + if (!isTuple(@TypeOf(args))) { + @compileError("argument must be tuple for withArgs"); + } + return self; + } + + pub fn delete(self: *Expectation) void { + if (self._deinit) |deinit_fn| { + deinit_fn(self); + } + } }; pub const MockTableType = struct { @@ -132,6 +207,9 @@ pub const MockTableType = struct { ._allocator = self.allocator, ._return = null, ._node = std.DoublyLinkedList.Node{}, + ._deinit = null, + ._times = 1, + ._args_matcher = null, }; _ = list.append(&expectation._node); return expectation; diff --git a/tests/mock_object.zig b/tests/mock_object.zig index ff616ce..a96ff9f 100644 --- a/tests/mock_object.zig +++ b/tests/mock_object.zig @@ -47,11 +47,11 @@ const IShape = interface.ConstructCountingInterface(struct { const MockShape = interface.DeriveFromBase(IShape, struct { const Self = @This(); - _mock: *interface.MockTableType, + mock: *interface.MockTableType, pub fn create() MockShape { return MockShape.init(.{ - ._mock = interface.GenerateMockTable(IShape, std.testing.allocator), + .mock = interface.GenerateMockTable(IShape, std.testing.allocator), }); } @@ -77,9 +77,24 @@ test "interface can be mocked for tests" { var obj = try mock.interface.new(std.testing.allocator); defer obj.interface.delete(); - mock.data()._mock + _ = mock.data().mock .expectCall("area") - .willReturn(@as(i32, 10)); + .willReturn(@as(i32, 10)) + .times(3); + _ = mock.data().mock + .expectCall("area") + .willReturn(@as(i32, 15)); + + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); try std.testing.expectEqual(10, obj.interface.area()); + + try std.testing.expectEqual(15, obj.interface.area()); + + _ = mock.data().mock + .expectCall("set_size") + .withArgs(.{@as(u32, 150)}); + + obj.interface.set_size(150); } From 381279058785404222eee00144beff79f7533e00 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:03:09 +0200 Subject: [PATCH 03/11] added mocking support --- .vscode/launch.json | 2 +- build.zig | 3 + src/interface.zig | 14 - src/mock.zig | 653 +++++++++++++++++++++++++++++++++--------- src/root.zig | 5 +- tests/mock_object.zig | 143 ++++++++- 6 files changed, 660 insertions(+), 160 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f5c0048..1c5dfc8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Debug Tests", "type": "lldb", "request": "launch", - "program": "${workspaceFolder}/zig-out/bin/fs_tests", + "program": "${workspaceFolder}/zig-out/bin/test", "cwd": "${workspaceRoot}", } ] diff --git a/build.zig b/build.zig index 74ffbfb..5524fa2 100644 --- a/build.zig +++ b/build.zig @@ -20,6 +20,7 @@ pub fn build(b: *std.Build) !void { const exe_tests = b.addTest(.{ .root_module = mod_tests, + .use_llvm = true, }); exe_tests.root_module.addImport("interface", mod); @@ -28,6 +29,8 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_exe_tests.step); + b.installArtifact(exe_tests); + if (enable_examples) { var iterable_dir = try std.fs.cwd().openDir("examples", .{ .iterate = true }); defer iterable_dir.close(); diff --git a/src/interface.zig b/src/interface.zig index e1ebc43..7f3c154 100644 --- a/src/interface.zig +++ b/src/interface.zig @@ -298,16 +298,6 @@ pub fn DeriveFromBase(comptime BaseType: anytype, comptime Derived: type) type { if (!@hasField(Derived, "base") or !(@FieldType(Derived, "base") == BaseType)) { @compileError("Deriving from a base instead of an interface requires a 'base' field in the derived type."); } - // // disallow fields override - // var base: ?type = BaseType; - // while (base != null) { - // for (std.meta.fields(Derived)) |field| { - // if (@hasField(base.?, field.name) and !std.mem.eql(u8, field.name, "base")) { - // @compileError("Field already exists in the base: " ++ field.name); - // } - // } - // base = base.?.Base; - // } }; return struct { @@ -331,10 +321,6 @@ pub fn DeriveFromBase(comptime BaseType: anytype, comptime Derived: type) type { pub fn data(self: *Self) *Derived { return &self.__data; } - - // pub fn __destructor(self: *Self) void { - // self.interface.__destructor(self.interface); - // } }; } diff --git a/src/mock.zig b/src/mock.zig index bb446e3..b9af0d0 100644 --- a/src/mock.zig +++ b/src/mock.zig @@ -21,36 +21,7 @@ const interface = @import("interface.zig"); const std = @import("std"); -fn generateMockMethod(comptime Method: type, comptime name: [:0]const u8) std.builtin.Type.StructField { - @compileLog("Generating mock method: ", name); - return std.builtin.Type.StructField{ - .name = name, - .type = Method, - .default_value_ptr = null, - .is_comptime = false, - .alignment = @alignOf(Method), - }; -} - -fn GenerateMockStruct(comptime InterfaceType: type) type { - comptime var fields: []const std.builtin.Type.StructField = &[_]std.builtin.Type.StructField{}; - inline for (std.meta.fields(InterfaceType)) |d| { - @compileLog("Checking method: ", d.name); - fields = fields ++ &[_]std.builtin.Type.StructField{generateMockMethod(d.type, d.name)}; - } - - return @Type(.{ .@"struct" = .{ - .layout = .auto, - .is_tuple = false, - .fields = fields, - .decls = &.{}, - } }); -} - -pub fn MockInterface(comptime InterfaceType: type) type { - const MockType = GenerateMockStruct(InterfaceType.VTable); - return interface.DeriveFromBase(InterfaceType, MockType); -} +pub const any = struct {}; fn deduce_type(comptime T: type) type { switch (@typeInfo(T)) { @@ -59,13 +30,71 @@ fn deduce_type(comptime T: type) type { } } -pub fn MockVirtualCall(self: anytype, comptime method_name: [:0]const u8, args: anytype, return_type: type) return_type { - _ = args; // Suppress unused variable warning - if (!@hasField(deduce_type(@TypeOf(self)), "mock")) { - @compileError("MockVirtualCall called on non-mock object, please add .mock: interface.GenerateMockTable() to the derived mock struct"); +fn find_best_expectation(ExpectationType: type, expectations: *std.DoublyLinkedList, args: anytype) !?*ExpectationHolder { + var best_score: i32 = -1; + var best_node: ?*std.DoublyLinkedList.Node = null; + + var it = expectations.first; + while (it) |list_node| { + const holder: *ExpectationHolder = @fieldParentPtr("_node", list_node); + const expectation = @as(*ExpectationType, @ptrCast(@alignCast(holder.expectation))); + + if (expectation._sequence) |seq| { + if (seq.isExpectationAllowed(expectation)) { + if (expectation.matchesArgs(args) > 0) { + return holder; + } + } + + // sequence is broken, but maybe we have other expectation that matches and do not require sequence + var itt = list_node.next; + while (itt) |n| { + const h: *ExpectationHolder = @fieldParentPtr("_node", n); + const e = @as(*ExpectationType, @ptrCast(@alignCast(h.expectation))); + if (e._sequence == null) { + const sc = e.matchesArgs(args); + if (sc > best_score) { + best_score = sc; + best_node = itt; + } + } + itt = n.next; + } + if (best_node != null) { + return @as(*ExpectationHolder, @fieldParentPtr("_node", best_node.?)); + } + + std.debug.print("Sequence was broken due to the call\n", .{}); + std.debug.dumpCurrentStackTrace(null); + std.debug.print("Expectation set at:\n", .{}); + std.debug.dumpStackTrace(expectation._stack_trace); + std.debug.print("Required in sequence call:\n", .{}); + seq.dumpExpectation(); + return error.SequenceBroken; + } + + var score = expectation.matchesArgs(args); + if (score == 0 and expectation._callback != null) { + score = 1; + } + + if (score > best_score) { + best_score = score; + best_node = list_node; + } + + it = list_node.next; + } + + if (best_node == null) { + return null; } - var expectations = self.mock.expectations.getPtr(method_name) orelse { + return @as(*ExpectationHolder, @fieldParentPtr("_node", best_node.?)); +} + +fn verify_mock_call(self: anytype, comptime method_name: [:0]const u8, args: anytype, ReturnType: type) ReturnType { + var expectations = self.expectations.getPtr(method_name) orelse { std.debug.print("No expectations found for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); unreachable; }; @@ -75,55 +104,157 @@ pub fn MockVirtualCall(self: anytype, comptime method_name: [:0]const u8, args: unreachable; } - const list_node = expectations.first orelse unreachable; - const expectation = @as(*Expectation, @fieldParentPtr("_node", list_node)); - expectation._times -= 1; - const ret = expectation.getReturnValue(return_type); - if (expectation._times == 0) { - defer self.mock.allocator.destroy(expectation); - if (expectation._return) |r| { - defer { - self.mock.allocator.destroy(@as(*return_type, @ptrCast(@alignCast(r)))); - expectation._return = null; + const maybe_holder = find_best_expectation(Expectation(@TypeOf(args), ReturnType), expectations, args) catch |err| { + switch (err) { + error.SequenceBroken => { + unreachable; + }, + else => unreachable, + } + }; + + if (maybe_holder) |holder| { + const expectation = @as(*Expectation(@TypeOf(args), ReturnType), @ptrCast(@alignCast(holder.expectation))); + var ret: ReturnType = undefined; + if (expectation._callback) |cb| { + ret = cb(args) catch unreachable; + } else { + ret = expectation.getReturnValue(); + } + + if (expectation._times) |*times| { + times.* -= 1; + if (times.* == 0) { + if (expectation._sequence) |seq| { + seq.popFirst(); + } + Expectation(@TypeOf(args), ReturnType).verify(expectation) catch unreachable; + + expectations.remove(&holder._node); + self.allocator.destroy(holder); } } - expectations.remove(list_node); + return ret; + } else { + std.debug.print("No matching expectation found for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), method_name }); + unreachable; } - return ret; } -pub fn MockDestructorCall(self: anytype) void { - if (!@hasField(deduce_type(@TypeOf(self)), "mock")) { - @compileError("MockVirtualCall called on non-mock object, please add .mock: interface.GenerateMockTable() to the derived mock struct"); - } - var it = self.mock.expectations.iterator(); +pub fn MockDestructorCall(self: anytype) !void { + var it = self.expectations.iterator(); + // clean expectation with any times call while (it.next()) |entry| { - if (entry.value_ptr.len() != 0) { - std.debug.print("Not all expectations were met for method: {s}.{s}\n", .{ @typeName(deduce_type(@TypeOf(self))), entry.key_ptr.* }); - } var list = entry.value_ptr; - while (list.pop()) |node| { - var expectation = @as(*Expectation, @fieldParentPtr("_node", node)); - expectation.delete(); + var node_it = list.first; + while (node_it) |list_node| { + const holder: *ExpectationHolder = @fieldParentPtr("_node", list_node); + try holder.verify(); + const next_node = list_node.next; + list.remove(list_node); + self.allocator.destroy(holder); + node_it = next_node; + } + } + self.expectations.deinit(); + if (@hasField(@TypeOf(self.interface), "__refcount")) { + if (self.interface.__refcount) |refcount| { + self.allocator.destroy(refcount); } } - self.mock.expectations.deinit(); - self.mock.allocator.destroy(self.mock); } +// Type-erased argument matcher that can store any type const ArgMatcher = struct { - alignment: u8, - value_ptr: *anyopaque, - match: *const fn (*anyopaque, *anyopaque) bool, - deinit: *const fn (*anyopaque) void, + value_ptr: ?*anyopaque, + match: ?*const fn (stored: *anyopaque, actual: *anyopaque) bool, + deinit: ?*const fn (value: *anyopaque, allocator: std.mem.Allocator) void, + + pub fn create(comptime T: type, allocator: std.mem.Allocator, value: anytype) !ArgMatcher { + if (@TypeOf(value) == any) { + return ArgMatcher{ + .value_ptr = null, + .match = null, + .deinit = null, + }; + } + + const ValueHolder = struct { + fn matchFn(stored: *anyopaque, actual: *anyopaque) bool { + const stored_val: *T = @ptrCast(@alignCast(stored)); + const actual_val: *T = @ptrCast(@alignCast(actual)); + return std.meta.eql(stored_val.*, actual_val.*); + } + + fn deinitFn(val: *anyopaque, alloc: std.mem.Allocator) void { + const typed_val: *T = @ptrCast(@alignCast(val)); + alloc.destroy(typed_val); + } + }; + + const stored = try allocator.create(T); + stored.* = value; + + return ArgMatcher{ + .value_ptr = stored, + .match = ValueHolder.matchFn, + .deinit = ValueHolder.deinitFn, + }; + } + + pub fn matches(self: *const ArgMatcher, value: anytype) i32 { + var val = value; + if (self.value_ptr) |value_ptr| { + return if (self.match.?(value_ptr, @ptrCast(&val))) 10 else 0; + } + return 1; + } + + pub fn destroy(self: *const ArgMatcher, allocator: std.mem.Allocator) void { + if (self.value_ptr) |value_ptr| { + self.deinit.?(value_ptr, allocator); + } + } }; const ArgsMatcher = struct { allocator: std.mem.Allocator, - args: std.ArrayList(ArgMatcher), + matchers: std.ArrayList(ArgMatcher), + + pub fn init(allocator: std.mem.Allocator) ArgsMatcher { + return .{ + .allocator = allocator, + .matchers = std.ArrayList(ArgMatcher).initCapacity(allocator, 0) catch unreachable, + }; + } + + pub fn addMatcher(self: *ArgsMatcher, comptime T: type, value: T) !void { + const matcher = try ArgMatcher.create(T, self.allocator, value); + try self.matchers.append(self.allocator, matcher); + } + + pub fn matchesArgs(self: *const ArgsMatcher, args: anytype) i32 { + const fields = std.meta.fields(@TypeOf(args)); + var total_score: i32 = 0; + if (fields.len != self.matchers.items.len) { + return 0; + } + + inline for (fields, 0..) |field, i| { + const value: i32 = self.matchers.items[i].matches(@field(args, field.name)); + if (value == 0) { + return 0; + } + total_score += value; + } + return total_score; + } pub fn deinit(self: *ArgsMatcher) void { - self.args.deinit(self.allocator); + for (self.matchers.items) |matcher| { + matcher.destroy(self.allocator); + } + self.matchers.deinit(self.allocator); } }; @@ -135,92 +266,352 @@ fn isTuple(comptime T: type) bool { }; } -const Expectation = struct { - _allocator: std.mem.Allocator, - _return: ?*anyopaque, +pub const ExpectationHolder = struct { _node: std.DoublyLinkedList.Node, - _deinit: ?*const fn (self: *Expectation) void, - _args_matcher: ArgsMatcher, - _times: i32, - - pub fn willReturn(self: *Expectation, value: anytype) *Expectation { - const return_object = self._allocator.create(@TypeOf(value)) catch unreachable; - return_object.* = value; - self._return = return_object; - const FunctorType = struct { - pub fn call(s: *Expectation) void { - if (s._return) |r| { - s._allocator.destroy(@as(*@TypeOf(value), @ptrCast(@alignCast(r)))); - s._return = null; + expectation: *anyopaque, + verify_fn: *const fn (*anyopaque) anyerror!void, + + pub fn verify(self: *ExpectationHolder) anyerror!void { + return self.verify_fn(self.expectation); + } +}; + +pub fn Expectation(comptime ArgsType: type, comptime ReturnType: type) type { + return struct { + const Self = @This(); + const CallbackFn = *const fn (ArgsType) anyerror!ReturnType; + + _allocator: std.mem.Allocator, + _return: ?ReturnType, + _args_matcher: ?ArgsMatcher, + _times: ?i32, + _callback: ?CallbackFn, + _stack_trace: std.builtin.StackTrace, + _sequence: ?*Sequence, + + pub fn init(allocator: std.mem.Allocator, stacktrace: std.builtin.StackTrace) Self { + return Self{ + ._allocator = allocator, + ._return = null, + ._args_matcher = null, + ._times = 1, + ._callback = null, + ._stack_trace = stacktrace, + ._sequence = null, + }; + } + + pub fn willReturn(self: *Self, value: ReturnType) *Self { + self._return = value; + return self; + } + + pub fn times(self: *Self, count: anytype) *Self { + if (@TypeOf(count) == any) { + self._times = null; + return self; + } + self._times = count; + return self; + } + + /// Set a callback function to invoke when this expectation is matched + /// The callback receives the arguments and can return a value + pub fn invoke(self: *Self, callback: CallbackFn) *Self { + self._callback = callback; + return self; + } + + pub fn getReturnValue(self: *Self) ReturnType { + // If callback is set, invoke it + // Otherwise return the stored value + if (ReturnType == void) { + return; + } + std.debug.assert(self._return != null); + return self._return.?; + } + + pub fn withArgs(self: *Self, args: anytype) *Self { + var matcher = ArgsMatcher.init(self._allocator); + const fields = std.meta.fields(@TypeOf(args)); + inline for (fields) |field| { + matcher.addMatcher(field.type, @field(args, field.name)) catch unreachable; + } + self._args_matcher = matcher; + return self; + } + + pub fn matchesArgs(self: *const Self, args: ArgsType) i32 { + if (self._args_matcher) |*matcher| { + return matcher.matchesArgs(args); + } + if (@TypeOf(args) == @TypeOf(.{})) { + return 1; + } + return 0; + } + + pub fn verify(ctx: *anyopaque) anyerror!void { + const self: *Self = @ptrCast(@alignCast(ctx)); + if (self._times) |t| { + errdefer { + std.debug.print("Expectation not met times: {d} for: \n", .{t}); + std.debug.dumpStackTrace(self._stack_trace); } + try std.testing.expectEqual(t, 0); + } + if (self._sequence) |seq| { + seq.release(); + } + if (self._args_matcher) |*matcher| { + matcher.deinit(); } + self._allocator.free(self._stack_trace.instruction_addresses); + self._allocator.destroy(self); + } + + pub fn dumpStackTrace(ctx: *anyopaque) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + std.debug.dumpStackTrace(self._stack_trace); + } + + pub fn inSequence(self: *Self, sequence: *Sequence) *Self { + self._sequence = sequence.share(); + sequence.addExpectation(self, dumpStackTrace) catch unreachable; + return self; + } + }; +} + +pub const Sequence = struct { + pub const DumpStacktraceFn = *const fn (ctx: *anyopaque) void; + const ExpectationNode = struct { + ptr: *anyopaque, + stacktrace: DumpStacktraceFn, + }; + allocator: std.mem.Allocator, + expectations: std.ArrayList(ExpectationNode), + ref_count: usize, + + pub fn init(allocator: std.mem.Allocator) Sequence { + return Sequence{ + .allocator = allocator, + .expectations = std.ArrayList(ExpectationNode).initCapacity(allocator, 0) catch unreachable, + .ref_count = 0, }; - self._deinit = &FunctorType.call; - return self; } - pub fn times(self: *Expectation, count: i32) *Expectation { - self._times = count; - return self; + pub fn addExpectation(self: *Sequence, expectation: *anyopaque, stacktrace: DumpStacktraceFn) !void { + try self.expectations.append(self.allocator, .{ .ptr = expectation, .stacktrace = stacktrace }); } - pub fn getReturnValue(self: *Expectation, return_type: type) return_type { - if (return_type == void) { + pub fn dumpExpectation(self: *Sequence) void { + if (self.expectations.items.len == 0) { + std.debug.print("No expectations in sequence\n", .{}); return; } - std.testing.expect(self._return != null) catch unreachable; - return @as(*return_type, @ptrCast(@alignCast(self._return.?))).*; + self.expectations.items[0].stacktrace(self.expectations.items[0].ptr); } - pub fn withArgs(self: *Expectation, args: anytype) *Expectation { - if (!isTuple(@TypeOf(args))) { - @compileError("argument must be tuple for withArgs"); + pub fn isExpectationAllowed(self: *Sequence, expectation: *anyopaque) bool { + if (self.expectations.items.len == 0) { + return false; } - return self; + return self.expectations.items[0].ptr == expectation; } - pub fn delete(self: *Expectation) void { - if (self._deinit) |deinit_fn| { - deinit_fn(self); + pub fn popFirst(self: *Sequence) void { + if (self.expectations.items.len == 0) { + return; } + _ = self.expectations.orderedRemove(0); } -}; -pub const MockTableType = struct { - allocator: std.mem.Allocator, - expectations: std.StringHashMap(std.DoublyLinkedList), - - pub fn init(allocator: std.mem.Allocator) MockTableType { - return MockTableType{ - .allocator = allocator, - .expectations = std.StringHashMap(std.DoublyLinkedList).init(allocator), - }; + pub fn share(self: *Sequence) *Sequence { + self.ref_count += 1; + return self; } - pub fn expectCall(self: *MockTableType, comptime method_name: [:0]const u8) *Expectation { - var list = self.expectations.getPtr(method_name) orelse blk: { - self.expectations.put(method_name, std.DoublyLinkedList{}) catch unreachable; - break :blk self.expectations.getPtr(method_name) orelse unreachable; - }; - const expectation = self.allocator.create(Expectation) catch unreachable; - expectation.* = Expectation{ - ._allocator = self.allocator, - ._return = null, - ._node = std.DoublyLinkedList.Node{}, - ._deinit = null, - ._times = 1, - ._args_matcher = null, - }; - _ = list.append(&expectation._node); - return expectation; + pub fn release(self: *Sequence) void { + self.ref_count -= 1; + if (self.ref_count == 0) { + self.expectations.deinit(self.allocator); + } } }; -pub fn GenerateMockTable(InterfaceType: type, allocator: std.mem.Allocator) *MockTableType { - var table = allocator.create(MockTableType) catch unreachable; - table.* = MockTableType.init(allocator); - inline for (std.meta.fields(InterfaceType.VTable)) |field| { - table.expectations.put(field.name, std.DoublyLinkedList{}) catch unreachable; - } - return table; +pub fn MockInterface(comptime InterfaceType: type) type { + return struct { + const Self = @This(); + allocator: std.mem.Allocator, + interface: InterfaceType, + expectations: std.StringHashMap(std.DoublyLinkedList), + + // Helper to extract function signature types from VTable + fn getMethodTypes(comptime method_name: [:0]const u8) struct { args: type, ret: type } { + const vtable_fields = std.meta.fields(InterfaceType.VTable); + inline for (vtable_fields) |field| { + if (std.mem.eql(u8, field.name, method_name)) { + const field_type_info = @typeInfo(field.type); + + // Handle optional pointer: ?*const fn(...) + const fn_info = switch (field_type_info) { + .optional => blk: { + const child_info = @typeInfo(field_type_info.optional.child); + break :blk switch (child_info) { + .pointer => @typeInfo(child_info.pointer.child).@"fn", + .@"fn" => child_info.@"fn", + else => @compileError("Expected function or function pointer in optional"), + }; + }, + .pointer => @typeInfo(field_type_info.pointer.child).@"fn", + .@"fn" => field_type_info.@"fn", + else => @compileError("Expected function, function pointer, or optional function pointer in VTable"), + }; + + const return_type = fn_info.return_type.?; + + // Build tuple type from parameters (skip self parameter) + comptime var arg_types: []const type = &[_]type{}; + inline for (fn_info.params[1..]) |param| { + arg_types = arg_types ++ [_]type{param.type.?}; + } + + return .{ .args = arg_types[0], .ret = return_type }; + } + } + @compileError("Method '" ++ method_name ++ "' not found in interface VTable"); + } + + pub fn expectCall(self: *Self, comptime method_name: [:0]const u8) *Expectation(getMethodTypes(method_name).args, getMethodTypes(method_name).ret) { + const types = getMethodTypes(method_name); + const ArgsType = types.args; + const ReturnType = types.ret; + + var list = self.expectations.getPtr(method_name) orelse blk: { + self.expectations.put(method_name, std.DoublyLinkedList{}) catch unreachable; + break :blk self.expectations.getPtr(method_name) orelse unreachable; + }; + + const ExpectationType = Expectation(ArgsType, ReturnType); + const expectation = self.allocator.create(ExpectationType) catch unreachable; + var stacktrace: std.builtin.StackTrace = .{ + .index = 0, + .instruction_addresses = self.allocator.alloc(usize, 32) catch unreachable, + }; + + std.debug.captureStackTrace(null, &stacktrace); + + expectation.* = ExpectationType.init(self.allocator, stacktrace); + + const holder = self.allocator.create(ExpectationHolder) catch unreachable; + holder.* = .{ + ._node = std.DoublyLinkedList.Node{}, + .expectation = expectation, + .verify_fn = ExpectationType.verify, + }; + + list.append(&holder._node); + return expectation; + } + + pub fn get_interface(self: *Self) *InterfaceType { + return &self.interface; + } + + pub fn create(allocator: std.mem.Allocator) !*Self { + const gen_vtable = struct { + const vtable = Self.__build_vtable_chain(); + }; + + const self = try allocator.create(Self); + + if (@hasField(InterfaceType, "__refcount")) { + const refcount: *i32 = try allocator.create(i32); + refcount.* = 1; + self.* = Self{ + .allocator = allocator, + .interface = InterfaceType{ + .__vtable = &gen_vtable.vtable, + .__ptr = self, + .__memfunctions = null, + .__refcount = refcount, + }, + .expectations = std.StringHashMap(std.DoublyLinkedList).init(allocator), + }; + inline for (std.meta.fields(InterfaceType.VTable)) |field| { + self.expectations.put(field.name, std.DoublyLinkedList{}) catch unreachable; + } + } else { + self.* = Self{ + .allocator = allocator, + .interface = InterfaceType{ + .__vtable = &gen_vtable.vtable, + .__ptr = self, + .__memfunctions = null, + }, + .expectations = std.StringHashMap(std.DoublyLinkedList).init(allocator), + }; + inline for (std.meta.fields(InterfaceType.VTable)) |field| { + self.expectations.put(field.name, std.DoublyLinkedList{}) catch unreachable; + } + } + return self; + } + + pub fn __build_vtable_chain() InterfaceType.VTable { + var vtable: InterfaceType.VTable = undefined; + + // Generate wrapper functions for each VTable entry + inline for (std.meta.fields(InterfaceType.VTable)) |field| { + const field_type_info = @typeInfo(field.type); + const fn_info = switch (field_type_info) { + .optional => blk: { + const child_info = @typeInfo(field_type_info.optional.child); + break :blk switch (child_info) { + .pointer => @typeInfo(child_info.pointer.child).@"fn", + .@"fn" => child_info.@"fn", + else => @compileError("Expected function or function pointer in optional"), + }; + }, + .pointer => @typeInfo(field_type_info.pointer.child).@"fn", + .@"fn" => field_type_info.@"fn", + else => @compileError("Expected function, function pointer, or optional function pointer in VTable"), + }; + + const SelfType = fn_info.params[0].type.?; + const ArgsType = fn_info.params[1].type.?; + + // Determine self pointer type (const or mutable, opaque) + const is_const = @typeInfo(SelfType).pointer.is_const; + + if (std.mem.eql(u8, field.name, "delete")) { + const Wrapper = struct { + fn call(ptr: SelfType, args: ArgsType) void { + _ = args; + const self: if (is_const) *const Self else *Self = @ptrCast(@alignCast(ptr)); + MockDestructorCall(self) catch unreachable; + } + }; + @field(vtable, field.name) = &Wrapper.call; + } else { + const Wrapper = struct { + fn call(ptr: SelfType, args: ArgsType) fn_info.return_type.? { + const self: if (is_const) *const Self else *Self = @ptrCast(@alignCast(ptr)); + return verify_mock_call(self, field.name, args, fn_info.return_type.?); + } + }; + + @field(vtable, field.name) = &Wrapper.call; + } + } + + return vtable; + } + + pub fn delete(self: *Self) void { + self.allocator.destroy(self); + } + }; } diff --git a/src/root.zig b/src/root.zig index 1612866..2c1b3c8 100644 --- a/src/root.zig +++ b/src/root.zig @@ -28,7 +28,4 @@ pub const CountingInterfaceDestructorCall = @import("interface.zig").CountingInt pub const base = @import("interface.zig").GetBase; -pub const MockVirtualCall = @import("mock.zig").MockVirtualCall; -pub const MockDestructorCall = @import("mock.zig").MockDestructorCall; -pub const GenerateMockTable = @import("mock.zig").GenerateMockTable; -pub const MockTableType = @import("mock.zig").MockTableType; +pub const mock = @import("mock.zig"); diff --git a/tests/mock_object.zig b/tests/mock_object.zig index a96ff9f..87af8e8 100644 --- a/tests/mock_object.zig +++ b/tests/mock_object.zig @@ -47,7 +47,7 @@ const IShape = interface.ConstructCountingInterface(struct { const MockShape = interface.DeriveFromBase(IShape, struct { const Self = @This(); - mock: *interface.MockTableType, + mock: *interface.MockTableType(IShape), pub fn create() MockShape { return MockShape.init(.{ @@ -72,19 +72,22 @@ const MockShape = interface.DeriveFromBase(IShape, struct { } }); +const ShapeMock = interface.mock.MockInterface(IShape); + test "interface can be mocked for tests" { - var mock = MockShape.InstanceType.create(); - var obj = try mock.interface.new(std.testing.allocator); + var mock = try ShapeMock.create(std.testing.allocator); + defer mock.delete(); + var obj = mock.get_interface(); defer obj.interface.delete(); - _ = mock.data().mock + _ = mock .expectCall("area") - .willReturn(@as(i32, 10)) + .willReturn(@as(u32, 10)) .times(3); - _ = mock.data().mock + _ = mock .expectCall("area") - .willReturn(@as(i32, 15)); + .willReturn(@as(u32, 15)); try std.testing.expectEqual(10, obj.interface.area()); try std.testing.expectEqual(10, obj.interface.area()); @@ -92,9 +95,129 @@ test "interface can be mocked for tests" { try std.testing.expectEqual(15, obj.interface.area()); - _ = mock.data().mock + _ = mock + .expectCall("set_size") + .withArgs(.{interface.mock.any{}}); + + _ = mock .expectCall("set_size") - .withArgs(.{@as(u32, 150)}); + .withArgs(.{@as(u32, 101)}); - obj.interface.set_size(150); + // best match selection is used here + obj.interface.set_size(101); + obj.interface.set_size(102); } + +test "mock can be called any times" { + const any = interface.mock.any{}; + var mock = try ShapeMock.create(std.testing.allocator); + defer mock.delete(); + var obj = mock.get_interface(); + defer obj.interface.delete(); + + _ = mock + .expectCall("area") + .willReturn(@as(u32, 10)) + .times(any); + + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); +} + +test "enforce sequence for tests" { + const any = interface.mock.any{}; + var sequence = interface.mock.Sequence.init(std.testing.allocator); + var mock = try ShapeMock.create(std.testing.allocator); + defer mock.delete(); + var obj = mock.get_interface(); + defer obj.interface.delete(); + + _ = mock + .expectCall("area") + .willReturn(@as(u32, 10)) + .times(3) + .inSequence(&sequence); + + _ = mock + .expectCall("set_size") + .withArgs(.{@as(u32, 101)}); + + _ = mock + .expectCall("set_size") + .withArgs(.{any}) + .inSequence(&sequence); + + _ = mock + .expectCall("area") + .willReturn(@as(u32, 15)) + .inSequence(&sequence); + + try std.testing.expectEqual(10, obj.interface.area()); + + obj.interface.set_size(101); + try std.testing.expectEqual(10, obj.interface.area()); + try std.testing.expectEqual(10, obj.interface.area()); + + obj.interface.set_size(102); + + try std.testing.expectEqual(15, obj.interface.area()); +} + +test "mock can invoke callback function" { + var mock = try ShapeMock.create(std.testing.allocator); + defer mock.delete(); + var obj = mock.get_interface(); + defer obj.interface.delete(); + + // // Test callback that computes the return value + // // Use std.meta.Tuple with empty array to get the correct type + const areaCallback = struct { + fn call(args: std.meta.Tuple(&[_]type{})) anyerror!u32 { + _ = args; + return 123; + } + }.call; + + _ = mock + .expectCall("area") + .invoke(areaCallback); + + try std.testing.expectEqual(123, obj.interface.area()); + + // Test callback with arguments + const setSizeCallback = struct { + fn call(args: std.meta.Tuple(&[_]type{u32})) anyerror!void { + try std.testing.expectEqual(@as(u32, 999), args[0]); + } + }.call; + + _ = mock + .expectCall("set_size") + .invoke(setSizeCallback); + + obj.interface.set_size(999); +} + +// test "mock invoke can be combined with withArgs" { +// var mock = try ShapeMock.create(std.testing.allocator); +// defer mock.delete(); +// var obj = mock.get_interface(); +// defer obj.interface.delete(); + +// const callback = struct { +// fn call(args: struct { u32 }) void { +// // Verify we got the expected argument +// std.testing.expectEqual(@as(u32, 42), args[0]) catch unreachable; +// } +// }.call; + +// _ = mock +// .expectCall("set_size") +// .withArgs(.{@as(u32, 42)}) +// .invoke(callback); + +// obj.interface.set_size(42); +// } From 38edcd1fdbe160011ff36bc1b9b0bbb038dc59c6 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:13:38 +0200 Subject: [PATCH 04/11] added workflow --- .github/workflows/unit_tests.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..bf6a559 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,25 @@ +name: oop.zig Unit Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + execute_unit_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - name: Install ZVM + shell: bash + run: | + curl -fsSL https://raw.githubusercontent.com/hendriknielaender/zvm/main/install.sh | bash + export PATH="/home/user/.local/share/zvm/bin:$PATH" + + - name: Execute tests + shell: bash + run: + zig build test --summary all From d240bc825810aab8241c51504b01358084f885f8 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:14:55 +0200 Subject: [PATCH 05/11] moved path exporting --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index bf6a559..6ebb057 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -17,9 +17,9 @@ jobs: shell: bash run: | curl -fsSL https://raw.githubusercontent.com/hendriknielaender/zvm/main/install.sh | bash - export PATH="/home/user/.local/share/zvm/bin:$PATH" - name: Execute tests shell: bash run: + export PATH="/home/user/.local/share/zvm/bin:$PATH" zig build test --summary all From 096d406e3159dec951b78224e051fd8ae6d2b2f0 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:15:31 +0200 Subject: [PATCH 06/11] typo --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 6ebb057..72a028b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -20,6 +20,6 @@ jobs: - name: Execute tests shell: bash - run: + run: | export PATH="/home/user/.local/share/zvm/bin:$PATH" zig build test --summary all From ed894edb4befb8410cac1931b7a20227177c5bdc Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:57:51 +0200 Subject: [PATCH 07/11] other zvm --- .github/workflows/unit_tests.yml | 5 +++-- build.zig.zon | 2 +- install_zig_with_zvm.sh | 7 +++++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100755 install_zig_with_zvm.sh diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 72a028b..ab87d94 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -16,10 +16,11 @@ jobs: - name: Install ZVM shell: bash run: | - curl -fsSL https://raw.githubusercontent.com/hendriknielaender/zvm/main/install.sh | bash + curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash - name: Execute tests shell: bash run: | - export PATH="/home/user/.local/share/zvm/bin:$PATH" + export PATH="$HOME/.zvm/bin:$PATH" + ./install_zig_with_zvm.sh zig build test --summary all diff --git a/build.zig.zon b/build.zig.zon index ddf406a..9394a32 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .oop_zig, .version = "0.0.1", .fingerprint = 0x4d0368551b1ab249, // Changing this has security and trust implications. - .minimum_zig_version = "0.16.0-dev.233+a0ec4e270", + .minimum_zig_version = "0.15.2", .dependencies = .{}, .paths = .{ "build.zig", diff --git a/install_zig_with_zvm.sh b/install_zig_with_zvm.sh new file mode 100755 index 0000000..ac595e9 --- /dev/null +++ b/install_zig_with_zvm.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +version=$(awk -F'"' '/\.minimum_zig_version/ { print $2 }' build.zig.zon) +echo "Installing Zig version $version using zvm..." + +zvm install $version +zvm use $version \ No newline at end of file From 7700e0cc27233e83c3827dadbf7ffdbd1823584c Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 16:59:26 +0200 Subject: [PATCH 08/11] f --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index ab87d94..bfccd7e 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -21,6 +21,6 @@ jobs: - name: Execute tests shell: bash run: | - export PATH="$HOME/.zvm/bin:$PATH" + export PATH="$HOME/.zvm/bin:$HOME/.zvm:$PATH" ./install_zig_with_zvm.sh zig build test --summary all From bd5ce74d160fc70651120b091be1ab552de65f46 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 17:00:35 +0200 Subject: [PATCH 09/11] f --- .github/workflows/unit_tests.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index bfccd7e..85d34ca 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -13,14 +13,11 @@ jobs: - uses: actions/checkout@v3 with: submodules: 'recursive' - - name: Install ZVM - shell: bash - run: | - curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash - - name: Execute tests shell: bash run: | + curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash + ls -la $HOME/.zvm export PATH="$HOME/.zvm/bin:$HOME/.zvm:$PATH" ./install_zig_with_zvm.sh zig build test --summary all From df7713e4d746a2112bbd28973fe3524f41aa6f61 Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 17:01:14 +0200 Subject: [PATCH 10/11] f --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 85d34ca..79e8afd 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -18,6 +18,6 @@ jobs: run: | curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash ls -la $HOME/.zvm - export PATH="$HOME/.zvm/bin:$HOME/.zvm:$PATH" + export PATH="$HOME/.zvm/bin:$HOME/.zvm/self:$PATH" ./install_zig_with_zvm.sh zig build test --summary all From cc256471c242cef8bdfbbbebb1c2d7ad0ab60acf Mon Sep 17 00:00:00 2001 From: Mateusz Stadnik Date: Sat, 25 Oct 2025 17:02:08 +0200 Subject: [PATCH 11/11] fixes --- .github/workflows/unit_tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 79e8afd..ad8be0b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -17,7 +17,6 @@ jobs: shell: bash run: | curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash - ls -la $HOME/.zvm export PATH="$HOME/.zvm/bin:$HOME/.zvm/self:$PATH" ./install_zig_with_zvm.sh zig build test --summary all