-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassertions.zig
More file actions
652 lines (597 loc) · 23.9 KB
/
Copy pathassertions.zig
File metadata and controls
652 lines (597 loc) · 23.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
const std = @import("std");
/// Custom error for assertion failures
pub const AssertionError = error{
AssertionFailed,
};
/// Expectation struct that holds the actual value and provides assertion methods
pub fn Expectation(comptime T: type) type {
return struct {
actual: T,
allocator: std.mem.Allocator,
negated: bool = false,
custom_message: ?[]const u8 = null,
const Self = @This();
/// Create a negated expectation
pub fn not(self: Self) Self {
return Self{
.actual = self.actual,
.allocator = self.allocator,
.negated = !self.negated,
.custom_message = self.custom_message,
};
}
/// Set a custom error message
pub fn withMessage(self: Self, message: []const u8) Self {
return Self{
.actual = self.actual,
.allocator = self.allocator,
.negated = self.negated,
.custom_message = message,
};
}
/// Assert strict equality (===)
pub fn toBe(self: Self, expected: T) !void {
const equal = std.meta.eql(self.actual, expected);
if (self.negated) {
if (equal) {
if (self.custom_message) |msg| {
std.debug.print("\nAssertion Failed: {s}\n", .{msg});
} else {
std.debug.print("\nExpected not to be equal\n", .{});
std.debug.print(" Received: {any}\n", .{self.actual});
}
return AssertionError.AssertionFailed;
}
} else {
if (!equal) {
if (self.custom_message) |msg| {
std.debug.print("\nAssertion Failed: {s}\n", .{msg});
} else {
std.debug.print("\nExpected values to be equal\n", .{});
std.debug.print(" Expected: {any}\n", .{expected});
std.debug.print(" Received: {any}\n", .{self.actual});
}
return AssertionError.AssertionFailed;
}
}
}
/// Assert deep equality
pub fn toEqual(self: Self, expected: T) !void {
// For now, toEqual is the same as toBe in Zig
// In more complex scenarios, this could handle deep struct comparison
try self.toBe(expected);
}
/// Assert value is true (for bools)
pub fn toBeTruthy(self: Self) !void {
if (T != bool) {
@compileError("toBeTruthy can only be used with boolean values");
}
if (self.negated) {
if (self.actual) {
std.debug.print("\nExpected value to be falsy but got true\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (!self.actual) {
std.debug.print("\nExpected value to be truthy but got false\n", .{});
return AssertionError.AssertionFailed;
}
}
}
/// Assert value is false (for bools)
pub fn toBeFalsy(self: Self) !void {
if (T != bool) {
@compileError("toBeFalsy can only be used with boolean values");
}
if (self.negated) {
if (!self.actual) {
std.debug.print("\nExpected value to be truthy but got false\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (self.actual) {
std.debug.print("\nExpected value to be falsy but got true\n", .{});
return AssertionError.AssertionFailed;
}
}
}
/// Assert value is null (for optionals)
pub fn toBeNull(self: Self) !void {
const type_info = @typeInfo(T);
if (type_info != .optional) {
@compileError("toBeNull can only be used with optional types");
}
const is_null = self.actual == null;
if (self.negated) {
if (is_null) {
std.debug.print("\nExpected value not to be null\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (!is_null) {
std.debug.print("\nExpected value to be null\n", .{});
std.debug.print(" Received: {any}\n", .{self.actual});
return AssertionError.AssertionFailed;
}
}
}
/// Assert value is not null (for optionals)
pub fn toBeDefined(self: Self) !void {
const type_info = @typeInfo(T);
if (type_info != .optional) {
// Non-optional types are always defined
if (self.negated) {
std.debug.print("\nExpected value to be null but it's a non-optional type\n", .{});
return AssertionError.AssertionFailed;
}
return;
}
const is_null = self.actual == null;
if (self.negated) {
if (!is_null) {
std.debug.print("\nExpected value to be undefined/null\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (is_null) {
std.debug.print("\nExpected value to be defined (not null)\n", .{});
return AssertionError.AssertionFailed;
}
}
}
/// Assert greater than
pub fn toBeGreaterThan(self: Self, expected: T) !void {
const type_info = @typeInfo(T);
const is_numeric = switch (type_info) {
.int, .float, .comptime_int, .comptime_float => true,
else => false,
};
if (!is_numeric) {
@compileError("toBeGreaterThan can only be used with numeric types");
}
const is_greater = self.actual > expected;
if (self.negated) {
if (is_greater) {
std.debug.print("\nExpected {any} not to be greater than {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
} else {
if (!is_greater) {
std.debug.print("\nExpected {any} to be greater than {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
}
}
/// Assert greater than or equal
pub fn toBeGreaterThanOrEqual(self: Self, expected: T) !void {
const type_info = @typeInfo(T);
const is_numeric = switch (type_info) {
.int, .float, .comptime_int, .comptime_float => true,
else => false,
};
if (!is_numeric) {
@compileError("toBeGreaterThanOrEqual can only be used with numeric types");
}
const is_gte = self.actual >= expected;
if (self.negated) {
if (is_gte) {
std.debug.print("\nExpected {any} not to be >= {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
} else {
if (!is_gte) {
std.debug.print("\nExpected {any} to be >= {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
}
}
/// Assert less than
pub fn toBeLessThan(self: Self, expected: T) !void {
const type_info = @typeInfo(T);
const is_numeric = switch (type_info) {
.int, .float, .comptime_int, .comptime_float => true,
else => false,
};
if (!is_numeric) {
@compileError("toBeLessThan can only be used with numeric types");
}
const is_less = self.actual < expected;
if (self.negated) {
if (is_less) {
std.debug.print("\nExpected {any} not to be less than {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
} else {
if (!is_less) {
std.debug.print("\nExpected {any} to be less than {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
}
}
/// Assert less than or equal
pub fn toBeLessThanOrEqual(self: Self, expected: T) !void {
const type_info = @typeInfo(T);
const is_numeric = switch (type_info) {
.int, .float, .comptime_int, .comptime_float => true,
else => false,
};
if (!is_numeric) {
@compileError("toBeLessThanOrEqual can only be used with numeric types");
}
const is_lte = self.actual <= expected;
if (self.negated) {
if (is_lte) {
std.debug.print("\nExpected {any} not to be <= {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
} else {
if (!is_lte) {
std.debug.print("\nExpected {any} to be <= {any}\n", .{ self.actual, expected });
return AssertionError.AssertionFailed;
}
}
}
/// Assert that a function throws an error
/// Usage: try expect(allocator, myFunction).toThrow()
pub fn toThrow(self: Self) !void {
// This function expects T to be a function that returns an error union
// Call the function and check if it returns an error
const result = if (@typeInfo(T) == .@"fn") blk: {
// For function types, we need to call them
break :blk self.actual();
} else blk: {
// For error unions, just use the value
break :blk self.actual;
};
if (self.negated) {
// Should NOT throw
if (result) |_| {
// No error thrown - this is what we want for .not()
return;
} else |_| {
std.debug.print("\nExpected function not to throw an error, but it did\n", .{});
return AssertionError.AssertionFailed;
}
} else {
// Should throw
if (result) |_| {
std.debug.print("\nExpected function to throw an error, but it succeeded\n", .{});
return AssertionError.AssertionFailed;
} else |_| {
// Error was thrown - this is what we want
return;
}
}
}
/// Assert that a function throws a specific error
/// Usage: try expect(allocator, myFunction).toThrowError(error.MyError)
pub fn toThrowError(self: Self, expected_error: anyerror) !void {
const result = if (@typeInfo(T) == .@"fn") blk: {
break :blk self.actual();
} else blk: {
break :blk self.actual;
};
if (self.negated) {
// Should NOT throw this specific error
if (result) |_| {
return; // No error, good
} else |err| {
if (err == expected_error) {
std.debug.print("\nExpected function not to throw error.{s}, but it did\n", .{@errorName(expected_error)});
return AssertionError.AssertionFailed;
}
return; // Different error, that's fine
}
} else {
// Should throw this specific error
if (result) |_| {
std.debug.print("\nExpected function to throw error.{s}, but it succeeded\n", .{@errorName(expected_error)});
return AssertionError.AssertionFailed;
} else |err| {
if (err != expected_error) {
std.debug.print("\nExpected function to throw error.{s}, but it threw error.{s}\n", .{ @errorName(expected_error), @errorName(err) });
return AssertionError.AssertionFailed;
}
return; // Correct error thrown
}
}
}
};
}
/// String-specific expectation methods
pub const StringExpectation = struct {
actual: []const u8,
allocator: std.mem.Allocator,
negated: bool = false,
custom_message: ?[]const u8 = null,
const Self = @This();
pub fn not(self: Self) Self {
return Self{
.actual = self.actual,
.allocator = self.allocator,
.negated = !self.negated,
.custom_message = self.custom_message,
};
}
pub fn withMessage(self: Self, message: []const u8) Self {
return Self{
.actual = self.actual,
.allocator = self.allocator,
.negated = self.negated,
.custom_message = message,
};
}
pub fn toBe(self: Self, expected: []const u8) !void {
const equal = std.mem.eql(u8, self.actual, expected);
if (self.negated) {
if (equal) {
if (self.custom_message) |msg| {
std.debug.print("\nAssertion Failed: {s}\n", .{msg});
} else {
std.debug.print("\nExpected strings not to be equal\n", .{});
std.debug.print(" Received: \"{s}\"\n", .{self.actual});
}
return AssertionError.AssertionFailed;
}
} else {
if (!equal) {
if (self.custom_message) |msg| {
std.debug.print("\nAssertion Failed: {s}\n", .{msg});
} else {
std.debug.print("\nExpected strings to be equal\n", .{});
std.debug.print(" Expected: \"{s}\"\n", .{expected});
std.debug.print(" Received: \"{s}\"\n", .{self.actual});
}
return AssertionError.AssertionFailed;
}
}
}
pub fn toEqual(self: Self, expected: []const u8) !void {
try self.toBe(expected);
}
pub fn toContain(self: Self, substring: []const u8) !void {
const contains = std.mem.indexOf(u8, self.actual, substring) != null;
if (self.negated) {
if (contains) {
std.debug.print("\nExpected \"{s}\" not to contain \"{s}\"\n", .{ self.actual, substring });
return AssertionError.AssertionFailed;
}
} else {
if (!contains) {
std.debug.print("\nExpected \"{s}\" to contain \"{s}\"\n", .{ self.actual, substring });
return AssertionError.AssertionFailed;
}
}
}
pub fn toStartWith(self: Self, prefix: []const u8) !void {
const starts_with = std.mem.startsWith(u8, self.actual, prefix);
if (self.negated) {
if (starts_with) {
std.debug.print("\nExpected \"{s}\" not to start with \"{s}\"\n", .{ self.actual, prefix });
return AssertionError.AssertionFailed;
}
} else {
if (!starts_with) {
std.debug.print("\nExpected \"{s}\" to start with \"{s}\"\n", .{ self.actual, prefix });
return AssertionError.AssertionFailed;
}
}
}
pub fn toEndWith(self: Self, suffix: []const u8) !void {
const ends_with = std.mem.endsWith(u8, self.actual, suffix);
if (self.negated) {
if (ends_with) {
std.debug.print("\nExpected \"{s}\" not to end with \"{s}\"\n", .{ self.actual, suffix });
return AssertionError.AssertionFailed;
}
} else {
if (!ends_with) {
std.debug.print("\nExpected \"{s}\" to end with \"{s}\"\n", .{ self.actual, suffix });
return AssertionError.AssertionFailed;
}
}
}
pub fn toHaveLength(self: Self, expected_length: usize) !void {
const actual_length = self.actual.len;
if (self.negated) {
if (actual_length == expected_length) {
std.debug.print("\nExpected string not to have length {d} but it does\n", .{expected_length});
return AssertionError.AssertionFailed;
}
} else {
if (actual_length != expected_length) {
std.debug.print("\nExpected string to have length {d} but got {d}\n", .{ expected_length, actual_length });
std.debug.print(" String: \"{s}\"\n", .{self.actual});
return AssertionError.AssertionFailed;
}
}
}
pub fn toBeEmpty(self: Self) !void {
const is_empty = self.actual.len == 0;
if (self.negated) {
if (is_empty) {
std.debug.print("\nExpected string not to be empty\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (!is_empty) {
std.debug.print("\nExpected string to be empty but got \"{s}\"\n", .{self.actual});
return AssertionError.AssertionFailed;
}
}
}
};
/// Slice expectation methods
pub fn SliceExpectation(comptime T: type) type {
return struct {
actual: []const T,
allocator: std.mem.Allocator,
negated: bool = false,
custom_message: ?[]const u8 = null,
const Self = @This();
pub fn not(self: Self) Self {
return Self{
.actual = self.actual,
.allocator = self.allocator,
.negated = !self.negated,
.custom_message = self.custom_message,
};
}
pub fn toHaveLength(self: Self, expected_length: usize) !void {
const actual_length = self.actual.len;
if (self.negated) {
if (actual_length == expected_length) {
std.debug.print("\nExpected slice not to have length {d} but it does\n", .{expected_length});
return AssertionError.AssertionFailed;
}
} else {
if (actual_length != expected_length) {
std.debug.print("\nExpected slice to have length {d} but got {d}\n", .{ expected_length, actual_length });
return AssertionError.AssertionFailed;
}
}
}
pub fn toContain(self: Self, item: T) !void {
var contains = false;
for (self.actual) |element| {
if (std.meta.eql(element, item)) {
contains = true;
break;
}
}
if (self.negated) {
if (contains) {
std.debug.print("\nExpected slice not to contain {any}\n", .{item});
return AssertionError.AssertionFailed;
}
} else {
if (!contains) {
std.debug.print("\nExpected slice to contain {any}\n", .{item});
return AssertionError.AssertionFailed;
}
}
}
pub fn toBeEmpty(self: Self) !void {
const is_empty = self.actual.len == 0;
if (self.negated) {
if (is_empty) {
std.debug.print("\nExpected slice not to be empty\n", .{});
return AssertionError.AssertionFailed;
}
} else {
if (!is_empty) {
std.debug.print("\nExpected slice to be empty but got length {d}\n", .{self.actual.len});
return AssertionError.AssertionFailed;
}
}
}
};
}
/// Create an expectation for a value
pub fn expect(allocator: std.mem.Allocator, actual: anytype) blk: {
const T = @TypeOf(actual);
if (T == []const u8) break :blk StringExpectation;
const type_info = @typeInfo(T);
if (type_info == .pointer) {
const ptr_info = type_info.pointer;
// String slice
if (ptr_info.size == .slice and ptr_info.child == u8) break :blk StringExpectation;
// String literal (pointer to array of u8)
const child_info = @typeInfo(ptr_info.child);
if (child_info == .array and child_info.array.child == u8) break :blk StringExpectation;
// Other u8 pointer
if (ptr_info.child == u8) break :blk StringExpectation;
// General slice
if (ptr_info.size == .slice) break :blk SliceExpectation(ptr_info.child);
}
break :blk Expectation(T);
} {
if (@TypeOf(actual) == []const u8) {
return StringExpectation{
.actual = actual,
.allocator = allocator,
};
}
const type_info = @typeInfo(@TypeOf(actual));
if (type_info == .pointer) {
const ptr_info = type_info.pointer;
// Check for u8 slices/pointers (strings)
if (ptr_info.child == u8) {
return StringExpectation{
.actual = actual,
.allocator = allocator,
};
}
// Check for array of u8 (string literals like "hello")
const child_info = @typeInfo(ptr_info.child);
if (child_info == .array and child_info.array.child == u8) {
return StringExpectation{
.actual = actual,
.allocator = allocator,
};
}
if (ptr_info.size == .slice) {
return SliceExpectation(ptr_info.child){
.actual = actual,
.allocator = allocator,
};
}
}
return Expectation(@TypeOf(actual)){
.actual = actual,
.allocator = allocator,
};
}
test "expect basic equality" {
const allocator = std.testing.allocator;
try expect(allocator, 5).toBe(5);
try expect(allocator, true).toBe(true);
}
test "expect with not modifier" {
const allocator = std.testing.allocator;
try expect(allocator, 5).not().toBe(3);
try expect(allocator, false).not().toBe(true);
}
test "expect string operations" {
const allocator = std.testing.allocator;
try expect(allocator, "hello").toBe("hello");
try expect(allocator, "hello world").toContain("world");
try expect(allocator, "hello").toStartWith("hel");
try expect(allocator, "hello").toEndWith("lo");
try expect(allocator, "hello").toHaveLength(5);
}
test "expect comparisons" {
const allocator = std.testing.allocator;
try expect(allocator, 10).toBeGreaterThan(5);
try expect(allocator, 10).toBeGreaterThanOrEqual(10);
try expect(allocator, 5).toBeLessThan(10);
try expect(allocator, 5).toBeLessThanOrEqual(5);
}
test "expect error assertions" {
const allocator = std.testing.allocator;
// Test function that throws an error
const ThrowsError = struct {
fn call() !void {
return error.TestError;
}
};
// Test function that succeeds
const Succeeds = struct {
fn call() !void {
return;
}
};
// Should throw any error
try expect(allocator, ThrowsError.call).toThrow();
// Should not throw
try expect(allocator, Succeeds.call).not().toThrow();
// Should throw specific error
try expect(allocator, ThrowsError.call).toThrowError(error.TestError);
// Should not throw specific error (throws different error)
const ThrowsDifferent = struct {
fn call() !void {
return error.DifferentError;
}
};
try expect(allocator, ThrowsDifferent.call).not().toThrowError(error.TestError);
}