const std = @import("std"); const StringHashMap = std.StringHashMap; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const GlobalAllocator = std.testing.allocator; const TailQueue = std.TailQueue; const warn = std.debug.warn; const gdt = @import("gdt_mock.zig"); const idt = @import("idt_mock.zig"); const cmos = @import("cmos_mock.zig"); /// /// The enumeration of types that the mocking framework supports. These include basic types like u8 /// and function types like fn () void /// const DataElementType = enum { BOOL, U4, U8, U16, U32, ECmosStatusRegister, ECmosRtcRegister, PTR_CONST_GdtPtr, PTR_CONST_IdtPtr, ERROR_IDTERROR_VOID, EFN_OVOID, NFN_OVOID, FN_OVOID, FN_OUSIZE, FN_OU16, FN_IU8_OBOOL, FN_IU8_OVOID, FN_IU16_OVOID, FN_IU16_OU8, FN_IU4_IU4_OU8, FN_IU8_IU8_OU16, FN_IU16_IU8_OVOID, FN_IU16_IU16_OVOID, FN_IECmosStatusRegister_IBOOL_OU8, FN_IECmosStatusRegister_IU8_IBOOL_OVOID, FN_IECmosRtcRegister_OU8, FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, FN_IU8_INFNOVOID_OERRORIDTERRORVOID, FN_IPTRCONSTGDTPTR_OVOID, FN_IPTRCONSTIDTPTR_OVOID, }; /// /// A tagged union of all the data elements that the mocking framework can work with. This can be /// expanded to add new types. This is needed as need a list of data that all have different types, /// so this wraps the data into a union, (which is of one type) so can have a list of them. /// When https://github.com/ziglang/zig/issues/383 anf https://github.com/ziglang/zig/issues/2907 /// is done, can programitaclly create types for this. Can use a compile time block that loops /// through the available basic types and create function types so don't have a long list. /// const DataElement = union(DataElementType) { BOOL: bool, U4: u4, U8: u8, U16: u16, U32: u32, ECmosStatusRegister: cmos.StatusRegister, ECmosRtcRegister: cmos.RtcRegister, PTR_CONST_GdtPtr: *const gdt.GdtPtr, PTR_CONST_IdtPtr: *const idt.IdtPtr, ERROR_IDTERROR_VOID: idt.IdtError!void, EFN_OVOID: extern fn () void, NFN_OVOID: fn () callconv(.Naked) void, FN_OVOID: fn () void, FN_OUSIZE: fn () usize, FN_OU16: fn () u16, FN_IU8_OBOOL: fn (u8) bool, FN_IU8_OVOID: fn (u8) void, FN_IU16_OVOID: fn (u16) void, FN_IU16_OU8: fn (u16) u8, FN_IU4_IU4_OU8: fn (u4, u4) u8, FN_IU8_IU8_OU16: fn (u8, u8) u16, FN_IU16_IU8_OVOID: fn (u16, u8) void, FN_IU16_IU16_OVOID: fn (u16, u16) void, FN_IECmosStatusRegister_IBOOL_OU8: fn (cmos.StatusRegister, bool) u8, FN_IECmosStatusRegister_IU8_IBOOL_OVOID: fn (cmos.StatusRegister, u8, bool) void, FN_IECmosRtcRegister_OU8: fn (cmos.RtcRegister) u8, FN_IU8_IEFNOVOID_OERRORIDTERRORVOID: fn (u8, extern fn () void) idt.IdtError!void, FN_IU8_INFNOVOID_OERRORIDTERRORVOID: fn (u8, fn () callconv(.Naked) void) idt.IdtError!void, FN_IPTRCONSTGDTPTR_OVOID: fn (*const gdt.GdtPtr) void, FN_IPTRCONSTIDTPTR_OVOID: fn (*const idt.IdtPtr) void, }; /// /// The type of actions that the mocking framework can perform. /// const ActionType = enum { /// This will test the parameters passed to a function. It will test the correct types and /// value of each parameter. This is also used to return a specific value from a function so /// can test for returns from a function. TestValue, /// This action is to replace a function call to be mocked with another function the user /// chooses to be replaced. This will consume the function call. This will allow the user to /// check that the function is called once or multiple times by added a function to be mocked /// multiple times. This also allows the ability for a function to be mocked by different /// functions each time it is called. ConsumeFunctionCall, /// This is similar to the ConsumeFunctionCall action, but will call the mocked function /// repeatedly until the mocking is done. RepeatFunctionCall, // Other actions that could be used // This will check that a function isn't called. //NoFunctionCall // This is a generalisation of ConsumeFunctionCall and RepeatFunctionCall but can specify how // many times a function can be called. //FunctionCallN }; /// /// This is a pair of action and data to be actioned on. /// const Action = struct { action: ActionType, data: DataElement, }; /// /// The type for a queue of actions using std.TailQueue. /// const ActionList = TailQueue(Action); /// /// The type for linking the function name to be mocked and the action list to be acted on. /// const NamedActionMap = StringHashMap(ActionList); /// /// The mocking framework. /// /// Return: type /// This returns a struct for adding and acting on mocked functions. /// fn Mock() type { return struct { const Self = @This(); /// The map of function name and action list. named_actions: NamedActionMap, /// /// Create a DataElement from data. This wraps data into a union. This allows the ability /// to have a list of different types. /// /// Arguments: /// IN arg: var - The data, this can be a function or basic type value. /// /// Return: DataElement /// A DataElement with the data wrapped. /// fn createDataElement(arg: var) DataElement { return switch (@TypeOf(arg)) { bool => DataElement{ .BOOL = arg }, u4 => DataElement{ .U4 = arg }, u8 => DataElement{ .U8 = arg }, u16 => DataElement{ .U16 = arg }, u32 => DataElement{ .U32 = arg }, cmos.StatusRegister => DataElement{ .ECmosStatusRegister = arg }, cmos.RtcRegister => DataElement{ .ECmosRtcRegister = arg }, *const gdt.GdtPtr => DataElement{ .PTR_CONST_GdtPtr = arg }, *const idt.IdtPtr => DataElement{ .PTR_CONST_IdtPtr = arg }, idt.IdtError!void => DataElement{ .ERROR_IDTERROR_VOID = arg }, extern fn () void => DataElement{ .EFN_OVOID = arg }, fn () callconv(.Naked) void => DataElement{ .NFN_OVOID = arg }, fn () void => DataElement{ .FN_OVOID = arg }, fn () usize => DataElement{ .FN_OUSIZE = arg }, fn () u16 => DataElement{ .FN_OU16 = arg }, fn (u8) bool => DataElement{ .FN_IU8_OBOOL = arg }, fn (u8) void => DataElement{ .FN_IU8_OVOID = arg }, fn (u16) void => DataElement{ .FN_IU16_OVOID = arg }, fn (u16) u8 => DataElement{ .FN_IU16_OU8 = arg }, fn (u4, u4) u8 => DataElement{ .FN_IU4_IU4_OU8 = arg }, fn (u8, u8) u16 => DataElement{ .FN_IU8_IU8_OU16 = arg }, fn (u16, u8) void => DataElement{ .FN_IU16_IU8_OVOID = arg }, fn (u16, u16) void => DataElement{ .FN_IU16_IU16_OVOID = arg }, fn (cmos.StatusRegister, bool) u8 => DataElement{ .FN_IECmosStatusRegister_IBOOL_OU8 = arg }, fn (cmos.StatusRegister, u8, bool) void => DataElement{ .FN_IECmosStatusRegister_IU8_IBOOL_OVOID = arg }, fn (cmos.RtcRegister) u8 => DataElement{ .FN_IECmosRtcRegister_OU8 = arg }, fn (*const gdt.GdtPtr) void => DataElement{ .FN_IPTRCONSTGDTPTR_OVOID = arg }, fn (*const idt.IdtPtr) void => DataElement{ .FN_IPTRCONSTIDTPTR_OVOID = arg }, fn (u8, extern fn () void) idt.IdtError!void => DataElement{ .FN_IU8_IEFNOVOID_OERRORIDTERRORVOID = arg }, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => DataElement{ .FN_IU8_INFNOVOID_OERRORIDTERRORVOID = arg }, else => @compileError("Type not supported: " ++ @typeName(@TypeOf(arg))), }; } /// /// Get the enum that represents the type given. /// /// Arguments: /// IN T: type - A type. /// /// Return: DataElementType /// The DataElementType that represents the type given. /// fn getDataElementType(comptime T: type) DataElementType { return switch (T) { bool => DataElementType.BOOL, u4 => DataElementType.U4, u8 => DataElementType.U8, u16 => DataElementType.U16, u32 => DataElementType.U32, cmos.StatusRegister => DataElementType.ECmosStatusRegister, cmos.RtcRegister => DataElementType.ECmosRtcRegister, *const gdt.GdtPtr => DataElement.PTR_CONST_GdtPtr, *const idt.IdtPtr => DataElement.PTR_CONST_IdtPtr, idt.IdtError!void => DataElement.ERROR_IDTERROR_VOID, extern fn () void => DataElementType.EFN_OVOID, fn () callconv(.Naked) void => DataElementType.NFN_OVOID, fn () void => DataElementType.FN_OVOID, fn () u16 => DataElementType.FN_OU16, fn (u8) bool => DataElementType.FN_IU8_OBOOL, fn (u8) void => DataElementType.FN_IU8_OVOID, fn (u16) void => DataElementType.FN_IU16_OVOID, fn (u16) u8 => DataElementType.FN_IU16_OU8, fn (u4, u4) u8 => DataElementType.FN_IU4_IU4_OU8, fn (u8, u8) u16 => DataElementType.FN_IU8_IU8_OU16, fn (u16, u8) void => DataElementType.FN_IU16_IU8_OVOID, fn (u16, u16) void => DataElementType.FN_IU16_IU16_OVOID, fn (cmos.StatusRegister, bool) u8 => DataElementType.FN_IECmosStatusRegister_IBOOL_OU8, fn (cmos.StatusRegister, u8, bool) void => DataElementType.FN_IECmosStatusRegister_IU8_IBOOL_OVOID, fn (cmos.RtcRegister) u8 => DataElementType.FN_IECmosRtcRegister_OU8, fn (*const gdt.GdtPtr) void => DataElementType.FN_IPTRCONSTGDTPTR_OVOID, fn (*const idt.IdtPtr) void => DataElementType.FN_IPTRCONSTIDTPTR_OVOID, fn (u8, extern fn () void) idt.IdtError!void => DataElementType.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => DataElementType.FN_IU8_INFNOVOID_OERRORIDTERRORVOID, else => @compileError("Type not supported: " ++ @typeName(T)), }; } /// /// Get the data out of the tagged union /// /// Arguments: /// IN T: type - The type of the data to extract. Used to switch on the /// tagged union. /// IN element: DataElement - The data element to unwrap the data from. /// /// Return: T /// The data of type T from the DataElement. /// fn getDataValue(comptime T: type, element: DataElement) T { return switch (T) { bool => element.BOOL, u4 => element.U4, u8 => element.U8, u16 => element.U16, u32 => element.U32, cmos.StatusRegister => element.ECmosStatusRegister, cmos.RtcRegister => element.ECmosRtcRegister, *const gdt.GdtPtr => element.PTR_CONST_GdtPtr, *const idt.IdtPtr => element.PTR_CONST_IdtPtr, idt.IdtError!void => element.ERROR_IDTERROR_VOID, extern fn () void => element.EFN_OVOID, fn () callconv(.Naked) void => element.NFN_OVOID, fn () void => element.FN_OVOID, fn () u16 => element.FN_OU16, fn (u8) bool => element.FN_IU8_OBOOL, fn (u8) void => element.FN_IU8_OVOID, fn (u16) void => element.FN_IU16_OVOID, fn (u16) u8 => element.FN_IU16_OU8, fn (u4, u4) u8 => element.FN_IU4_IU4_OU8, fn (u8, u8) u16 => element.FN_IU8_IU8_OU16, fn (u16, u8) void => element.FN_IU16_IU8_OVOID, fn (u16, u16) void => element.FN_IU16_IU16_OVOID, fn (cmos.StatusRegister, bool) u8 => element.FN_IECmosStatusRegister_IBOOL_OU8, fn (cmos.StatusRegister, u8, bool) void => element.FN_IECmosStatusRegister_IU8_IBOOL_OVOID, fn (cmos.RtcRegister) u8 => element.FN_IECmosRtcRegister_OU8, fn (*const gdt.GdtPtr) void => element.FN_IPTRCONSTGDTPTR_OVOID, fn (*const idt.IdtPtr) void => element.FN_IPTRCONSTIDTPTR_OVOID, fn (u8, extern fn () void) idt.IdtError!void => element.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => element.FN_IU8_INFNOVOID_OERRORIDTERRORVOID, else => @compileError("Type not supported: " ++ @typeName(T)), }; } /// /// Create a function type from a return type and its arguments. Waiting for /// https://github.com/ziglang/zig/issues/313. TODO: Tidy mocking framework #69 /// /// Arguments: /// IN RetType: type - The return type of the function. /// IN params: var - The argument list for the function. /// /// Return: type /// A function type that represents the return type and its arguments. /// fn getFunctionType(comptime RetType: type, params: var) type { return switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, else => @compileError("Couldn't generate function type for " ++ len ++ " parameters\n"), }; } /// /// This tests a value passed to a function. /// /// Arguments: /// IN ExpectedType: type - The expected type of the value to be tested. /// IN expected_value: ExpectedType - The expected value to be tested. This is what was /// passed to the functions. /// IN elem: DataElement - The wrapped data element to test against the /// expected value. /// fn expectTest(comptime ExpectedType: type, expected_value: ExpectedType, elem: DataElement) void { if (ExpectedType == void) { // Can't test void as it has no value std.debug.panic("Can not test a value for void\n", .{}); } // Test that the types match const expect_type = comptime getDataElementType(ExpectedType); expectEqual(expect_type, @as(DataElementType, elem)); // Types match, so can use the expected type to get the actual data const actual_value = getDataValue(ExpectedType, elem); // Test the values expectEqual(expected_value, actual_value); } /// /// This returns a value from the wrapped data element. This will be a test value to be /// returned by a mocked function. /// /// Arguments: /// IN fun_name: []const u8 - The function name to be used to tell the user if /// there is no return value set up. /// IN/OUT action_list: *ActionList - The action list to extract the return value from. /// IN DataType: type - The type of the return value. /// fn expectGetValue(comptime fun_name: []const u8, action_list: *ActionList, comptime DataType: type) DataType { if (DataType == void) { return; } if (action_list.*.popFirst()) |action_node| { const action = action_node.data; const expect_type = getDataElementType(DataType); const ret = getDataValue(DataType, action.data); expectEqual(@as(DataElementType, action.data), expect_type); // Free the node action_list.*.destroyNode(action_node, GlobalAllocator); return ret; } else { std.debug.panic("No more test values for the return of function: " ++ fun_name ++ "\n", .{}); } } /// /// This adds a action to the action list with ActionType provided. It will create a new /// mapping if one doesn't exist for a function name. /// /// Arguments: /// IN/OUT self: *Self - Self. This is the mocking object to be modified to add /// the test data. /// IN fun_name: []const u8 - The function name to add the test parameters to. /// IN data: var - The data to add. /// IN action_type: ActionType - The action type to add. /// pub fn addAction(self: *Self, comptime fun_name: []const u8, data: var, action_type: ActionType) void { // Add a new mapping if one doesn't exist. if (!self.named_actions.contains(fun_name)) { expect(self.named_actions.put(fun_name, TailQueue(Action).init()) catch unreachable == null); } // Get the function mapping to add the parameter to. if (self.named_actions.get(fun_name)) |actions_kv| { var action_list = actions_kv.value; const action = Action{ .action = action_type, .data = createDataElement(data), }; var a = action_list.createNode(action, GlobalAllocator) catch unreachable; action_list.append(a); // Need to re-assign the value as it isn't updated when we just append actions_kv.value = action_list; } else { // Shouldn't get here as we would have just added a new mapping // But just in case ;) std.debug.panic("No function name: " ++ fun_name ++ "\n", .{}); } } /// /// Perform an action on a function. This can be one of ActionType. /// /// Arguments: /// IN/OUT self: *Self - Self. This is the mocking object to be modified to /// perform a action. /// IN fun_name: []const u8 - The function name to act on. /// IN RetType: type - The return type of the function being mocked. /// IN params: var - The list of parameters of the mocked function. /// /// Return: RetType /// The return value of the mocked function. This can be void. /// pub fn performAction(self: *Self, comptime fun_name: []const u8, comptime RetType: type, params: var) RetType { if (self.named_actions.get(fun_name)) |kv_actions_list| { var action_list = kv_actions_list.value; // Peak the first action to test the action type if (action_list.first) |action_node| { const action = action_node.data; const ret = switch (action.action) { ActionType.TestValue => ret: { comptime var i = 0; inline while (i < params.len) : (i += 1) { // Now pop the action as we are going to use it // Have already checked that it is not null const test_node = action_list.popFirst().?; const test_action = test_node.data; const param = params[i]; const param_type = @TypeOf(params[i]); expectTest(param_type, param, test_action.data); // Free the node action_list.destroyNode(test_node, GlobalAllocator); } break :ret expectGetValue(fun_name, &action_list, RetType); }, ActionType.ConsumeFunctionCall => ret: { // Now pop the action as we are going to use it // Have already checked that it is not null const test_node = action_list.popFirst().?; const test_element = test_node.data.data; // Work out the type of the function to call from the params and return type // At compile time //const expected_function = getFunctionType(RetType, params); // Waiting for this: // error: compiler bug: unable to call var args function at compile time. https://github.com/ziglang/zig/issues/313 // to be resolved comptime const param_len = [1]u8{switch (params.len) { 0...9 => params.len + @as(u8, '0'), else => unreachable, }}; const expected_function = switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, 3 => fn (@TypeOf(params[0]), @TypeOf(params[1]), @TypeOf(params[2])) RetType, else => @compileError("Couldn't generate function type for " ++ param_len ++ " parameters\n"), }; // Get the corresponding DataElementType const expect_type = comptime getDataElementType(expected_function); // Test that the types match expectEqual(expect_type, @as(DataElementType, test_element)); // Types match, so can use the expected type to get the actual data const actual_function = getDataValue(expected_function, test_element); // Free the node action_list.destroyNode(test_node, GlobalAllocator); // The data element will contain the function to call const r = switch (params.len) { 0 => actual_function(), 1 => actual_function(params[0]), 2 => actual_function(params[0], params[1]), 3 => actual_function(params[0], params[1], params[2]), else => @compileError(param_len ++ " or more parameters not supported"), }; break :ret r; }, ActionType.RepeatFunctionCall => ret: { // Do the same for ActionType.ConsumeFunctionCall but instead of // popping the function, just peak const test_element = action.data; comptime const param_len = [1]u8{switch (params.len) { 0...9 => params.len + @as(u8, '0'), else => unreachable, }}; const expected_function = switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, 3 => fn (@TypeOf(params[0]), @TypeOf(params[1]), @TypeOf(params[2])) RetType, else => @compileError("Couldn't generate function type for " ++ param_len ++ " parameters\n"), }; // Get the corresponding DataElementType const expect_type = comptime getDataElementType(expected_function); // Test that the types match expectEqual(expect_type, @as(DataElementType, test_element)); // Types match, so can use the expected type to get the actual data const actual_function = getDataValue(expected_function, test_element); // The data element will contain the function to call const r = switch (params.len) { 0 => actual_function(), 1 => actual_function(params[0]), 2 => actual_function(params[0], params[1]), 3 => actual_function(params[0], params[1], params[2]), else => @compileError(param_len ++ " or more parameters not supported"), }; break :ret r; }, }; // Re-assign the action list as this would have changed kv_actions_list.value = action_list; return ret; } else { std.debug.panic("No action list elements for function: " ++ fun_name ++ "\n", .{}); } } else { std.debug.panic("No function name: " ++ fun_name ++ "\n", .{}); } } /// /// Initialise the mocking framework. /// /// Return: Self /// An initialised mocking framework. /// pub fn init() Self { return Self{ .named_actions = StringHashMap(ActionList).init(GlobalAllocator), }; } /// /// End the mocking session. This will check all test parameters and consume functions are /// consumed. Any repeat functions are deinit. /// /// Arguments: /// IN/OUT self: *Self - Self. This is the mocking object to be modified to finished /// the mocking session. /// pub fn finish(self: *Self) void { // Make sure the expected list is empty var it = self.named_actions.iterator(); while (it.next()) |next| { var action_list = next.value; if (action_list.popFirst()) |action_node| { const action = action_node.data; switch (action.action) { ActionType.TestValue, ActionType.ConsumeFunctionCall => { // These need to be all consumed std.debug.panic("Unused testing value: Type: {}, value: {} for function '{}'\n", .{ action.action, @as(DataElementType, action.data), next.key }); }, ActionType.RepeatFunctionCall => { // As this is a repeat action, the function will still be here // So need to free it action_list.destroyNode(action_node, GlobalAllocator); next.value = action_list; }, } } } // Free the function mapping self.named_actions.deinit(); } }; } /// The global mocking object that is used for a mocking session. Maybe in the future, we can have /// local mocking objects so can run the tests in parallel. var mock: ?Mock() = null; /// /// Get the mocking object and check we have one initialised. /// /// Return: *Mock() /// Pointer to the global mocking object so can be modified. /// fn getMockObject() *Mock() { // Make sure we have a mock object if (mock) |*m| { return m; } else { warn("MOCK object doesn't exists, please initiate this test\n", .{}); expect(false); unreachable; } } /// /// Initialise the mocking framework. /// pub fn initTest() void { // Make sure there isn't a mock object if (mock) |_| { warn("MOCK object already exists, please free previous test\n", .{}); expect(false); unreachable; } else { mock = Mock().init(); } } /// /// End the mocking session. This will check all test parameters and consume functions are /// consumed. Any repeat functions are deinit. /// pub fn freeTest() void { getMockObject().finish(); // This will stop double frees mock = null; } /// /// Add a list of test parameters to the action list. This will create a list of data /// elements that represent the list of parameters that will be passed to a mocked /// function. A mocked function may be called multiple times, so this list may contain /// multiple values for each call to the same mocked function. /// /// Arguments: /// IN/OUT self: *Self - Self. This is the mocking object to be modified to add /// the test parameters. /// IN fun_name: []const u8 - The function name to add the test parameters to. /// IN params: arglist - The parameters to add. /// pub fn addTestParams(comptime fun_name: []const u8, params: var) void { var mock_obj = getMockObject(); comptime var i = 0; inline while (i < params.len) : (i += 1) { mock_obj.addAction(fun_name, params[i], ActionType.TestValue); } } /// /// Add a function to mock out another. This will add a consume function action, so once /// the mocked function is called, this action wil be removed. /// /// Arguments: /// IN fun_name: []const u8 - The function name to add the function to. /// IN function: var - The function to add. /// pub fn addConsumeFunction(comptime fun_name: []const u8, function: var) void { getMockObject().addAction(fun_name, function, ActionType.ConsumeFunctionCall); } /// /// Add a function to mock out another. This will add a repeat function action, so once /// the mocked function is called, this action wil be removed. /// /// Arguments: /// IN fun_name: []const u8 - The function name to add the function to. /// IN function: var - The function to add. /// pub fn addRepeatFunction(comptime fun_name: []const u8, function: var) void { getMockObject().addAction(fun_name, function, ActionType.RepeatFunctionCall); } /// /// Perform an action on a function. This can be one of ActionType. /// /// Arguments: /// IN fun_name: []const u8 - The function name to act on. /// IN RetType: type - The return type of the function being mocked. /// IN params: var - The list of parameters of the mocked function. /// /// Return: RetType /// The return value of the mocked function. This can be void. /// pub fn performAction(comptime fun_name: []const u8, comptime RetType: type, params: var) RetType { return getMockObject().performAction(fun_name, RetType, params); }