summaryrefslogtreecommitdiffstats
path: root/source/test.c
blob: 2013e04a10beb35ef38b9c298060e426024e9558 (plain) (blame)
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
#include <assert.h>
#include <stdio.h>
#include <gccore.h>
#include <wiiuse/wpad.h>
#include <stdlib.h>
#include <stdarg.h>

#define TEST_INTERNAL
#include "test.h"

typedef u32 et_test_status;

#define test_current(_plan) ((_plan)->tests + (_plan)->current_test)

static int crit_test_fail_init(const struct et_state *state, void *data, et_state_init_data init_data) {
  struct et_test_plan *plan = init_data.p;
  struct et_test_plan_entry *last_ent;

  *(void **)data = plan;

  assert(plan->current_test > 0); /* NOTE: current_test has been incremented by this point */

  last_ent = plan->tests + (plan->current_test - 1);

  printf("\nCritical test failed: [%4s] %s\n", et_test_get_status_mnemonic(last_ent->status), last_ent->test->name);
  if (last_ent->ext_status) {
    printf("Test says:\n%s\n", last_ent->ext_status);
  } else {
    printf("No extended status.\n");
  }

  putchar('\n');

  printf("Press A to continue.\n");
  printf("Press B to finish testing.\n");
  printf("Press HOME/START to exit.\n");

  return 0;
}

static int crit_test_fail_tick(const struct et_state *state, void *data, struct et_next_state *next_state) {
  struct et_test_plan *plan = *(void **)data;
  u32 wpad_pressed = WPAD_ButtonsDown(0);
  u32 pad_pressed = PAD_ButtonsDown(0);

  if (wpad_pressed & WPAD_BUTTON_A || pad_pressed & PAD_BUTTON_A) {
    next_state->state = et_state_begin_testing; /* also works to resume testing */
    next_state->init_data.p = plan; /* test plan */
    return 1;
  }

  if (wpad_pressed & WPAD_BUTTON_B || pad_pressed & PAD_BUTTON_B) {
    next_state->state = plan->next_state;
    next_state->init_data = plan->next_state_init;
    return 1;
  }

  if (wpad_pressed & WPAD_BUTTON_HOME || pad_pressed & PAD_BUTTON_START) {
    printf("Goodbye!\n");
    exit(0);
  }

  return 0;
}

static void crit_test_fail_cleanup(const struct et_state *state, void *data) {
  /* do nothing */
}

static const struct et_state state_crit_test_fail = {
  .private_size = sizeof(struct et_test_plan *),
  .init_f = crit_test_fail_init,
  .tick_f = crit_test_fail_tick,
  .cleanup_f = crit_test_fail_cleanup
};

int test_next(struct et_test_plan *plan, struct et_next_state *next_state) {
  /* NOTE: this function doesn't increment current_test because the incremented value
   * would be visible in the cleanup function of the test state. */

  /* NOTE: this condition will not trigger for states that leak their TEST_PRIMORDIAL status.
   * I don't care about this situation so much, since that should only happen for bugged tests. */
  const struct et_test_plan_entry *ent = test_current(plan);
  switch (TEST_STATUS_GET_TYPE(ent->status)) {
    case TEST_SERROR:
    case TEST_SFAIL:
      if (ent->test->flags & TEST_PAUSE_ON_ERROR) {
        next_state->state = &state_crit_test_fail;
        next_state->init_data.p = plan;
        return 1;
      }
      break;
    default:
      /* do nothing */
      (void)0;
  }

  if (plan->current_test == plan->num_tests - 1) {
    /* this is the last state... we're done! */
    next_state->state = plan->next_state;
    next_state->init_data = plan->next_state_init;
  } else {
    next_state->state = &plan->tests[plan->current_test + 1].test->parent;
    next_state->init_data.p = plan;
  }

  return 1; /* always return 1: advance to next state */
}

int test_state_init(const struct et_state *state, void *data, et_state_init_data init_data) {
  struct et_test_plan *plan = init_data.p;
  struct et_test_state_private_base *testdata = data;
  struct et_test_plan_entry *cur = test_current(plan);
  const struct et_test *test = (const struct et_test *)state;

  testdata->plan = plan;

  assert(cur->test == test); /* ensure correct test was loaded */
  assert(TEST_STATUS_IS_CLEAN(cur->status));

  if (test->test_init_f) {
    return test->test_init_f(state, data, init_data);
  }

  return 0;
}

void test_state_cleanup(const struct et_state *state, void *data) {
  struct et_test_state_private_base *testdata = data;
  struct et_test_plan_entry *cur = test_current(testdata->plan);
  const struct et_test *test = (const struct et_test *)state;

  if (test->test_cleanup_f) {
    test->test_cleanup_f(state, data);
  }

  if (TEST_STATUS_IS_CLEAN(cur->status)) {
    /* test didn't set status :( */
    test_plan_entry_set_error_static(cur, "test aborted");
    cur->status = TEST_ERROR(TEST_EABORT);
  }

  printf("[%4s] %s\n", et_test_get_status_mnemonic(cur->status), test->name);

  ++testdata->plan->current_test;
}

static int begin_testing_init(const struct et_state *state, void *state_data, et_state_init_data init_data) {
  struct et_test_plan *test_plan = init_data.p;
  struct et_test_plan **pst_test_plan = state_data;
  *pst_test_plan = test_plan;
  return 0;
}

static int begin_testing_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) {
  struct et_test_plan **ptest_plan = state_data;
  struct et_test_plan *test_plan = *ptest_plan;

  assert(test_plan->current_test <= test_plan->num_tests);

  if (test_plan->current_test < test_plan->num_tests) {
    const struct et_test_plan_entry *next_test = test_plan->tests + test_plan->current_test;
    assert(test_plan->tests);
    assert(next_test->status == TEST_PRIMORDIAL);

    next_state->state = &next_test->test->parent;
    next_state->init_data.p = test_plan;
  } else {
    /* no tests to run in this plan */
    next_state->state = test_plan->next_state;
    next_state->init_data = test_plan->next_state_init;
  }

  return 1;

}

static void begin_testing_cleanup(const struct et_state *state, void *state_data) {
  /* do nothing */
}

static const struct et_state state_begin_testing_struct = {
  .private_size = sizeof(const struct et_test_plan *),
  .init_f = begin_testing_init,
  .tick_f = begin_testing_tick,
  .cleanup_f = begin_testing_cleanup
};

const struct et_state *et_state_begin_testing = &state_begin_testing_struct;

static int summ_test_plan_init(const struct et_state *state, void *state_data, et_state_init_data init_data) {
  struct et_test_plan *ptest_plan = (*(struct et_test_plan **)state_data = init_data.p);
  size_t passed = 0;
  size_t failed = 0;
  size_t errored = 0;
  size_t skipped = 0;
  size_t unknown = 0;

  for (size_t i = 0; i < ptest_plan->num_tests; ++i) {
    switch (TEST_STATUS_GET_TYPE(ptest_plan->tests[i].status)) {
      case TEST_SPASS: ++passed; break;
      case TEST_SFAIL: ++failed; break;
      case TEST_SERROR: ++errored; break;
      case TEST_SSKIPPED: ++skipped; break;
      default: ++unknown; break;
    }
  }

  printf("\nTesting complete.\nPASS: %zu, FAIL: %zu, ERROR: %zu, SKIP: %zu, UNK: %zu\n\n", passed, failed, errored, skipped, unknown);

  puts("Press A to test again. Press HOME/START to exit.");

  return 0;
}

static int summ_test_plan_tick(const struct et_state *state, void *state_data, struct et_next_state *next_state) {
  u32 wpad_buttons = WPAD_ButtonsDown(0);
  u32 pad_buttons = PAD_ButtonsDown(0);

  if (wpad_buttons & WPAD_BUTTON_A || pad_buttons & PAD_BUTTON_A) {
    next_state->state = et_state_setup;
    next_state->init_data = et_init_data_null;
    return 1;
  }

  if (wpad_buttons & WPAD_BUTTON_HOME || pad_buttons & PAD_BUTTON_START) {
    puts("Goodbye!");
    exit(0);
  }

  return 0;
}

static void summ_test_plan_cleanup(const struct et_state *state, void *state_data) {
  struct et_test_plan *ptest_plan = *(void **)state_data;

  for (size_t i = 0; i < ptest_plan->num_tests; ++i) {
    et_test_plan_entry_cleanup(ptest_plan->tests + i);
  }

  free(ptest_plan->tests);
  free(ptest_plan);
}

static const struct et_state summ_test_plan_struct = {
  .private_size = sizeof(struct et_test_plan *),
  .init_f = summ_test_plan_init,
  .tick_f = summ_test_plan_tick,
  .cleanup_f = summ_test_plan_cleanup
};

const struct et_state *et_state_summarize_test_plan = &summ_test_plan_struct;

#define COUNT_TESTS(_x) 1
#define NUM_TESTS FOREACH_TEST(COUNT_TESTS, +)
static const struct et_test *all_tests[NUM_TESTS + 1];
static bool tests_array_initialized = false;

const struct et_test *const *et_get_tests(size_t *ntests) {
  size_t idx = 0;

  if (ntests) *ntests = NUM_TESTS;
  if (tests_array_initialized) return all_tests;

#define A(_name) all_tests[idx++] = TEST_SYM(_name)
  FOREACH_TEST(A, ;);
#undef A

  all_tests[idx] = NULL;

  tests_array_initialized = true;
  return all_tests;
}

static const char *error_oom = "(failed to allocate error message)";
static const char *error_bad_fmt = "(bad extended error format)";

void et_test_plan_entry_cleanup(struct et_test_plan_entry *entry) {
  if (entry->ext_status_free) {
    /* okay to cast away const here. we're careful about making sure this pointer is non-NULL iff this pointer
     * was malloc'd */
    entry->ext_status_free((void *)entry->ext_status);
  }

  entry->ext_status = NULL;
  entry->ext_status_free = NULL;
}

void test_plan_entry_set_error(struct et_test_plan_entry *entry, const char *fmt, ...) {
  int res;
  va_list vp;
  char *dest;
  size_t dest_sz;

  et_test_plan_entry_cleanup(entry);

  va_start(vp, fmt);
  res = vsnprintf(NULL, 0, fmt, vp);
  va_end(vp);

  if (res < 0) {
    entry->ext_status = error_bad_fmt;
    entry->ext_status_free = NULL;
    return;
  }

  dest_sz = MIN((size_t)res + 1, (size_t)1024);
  dest = malloc(dest_sz);
  dest[0] = '\0';

  if (!dest) {
    entry->ext_status = error_oom;
    entry->ext_status_free = NULL;
    return;
  }

  va_start(vp, fmt);
  res = vsnprintf(dest, dest_sz, fmt, vp);
  va_end(vp);

  dest[dest_sz - 1] = '\0';

  assert(res >= 0);

  entry->ext_status = dest;
  entry->ext_status_free = free;
}

void test_plan_entry_set_error_static(struct et_test_plan_entry *entry, const char *str) {
  et_test_plan_entry_cleanup(entry);
  entry->ext_status = str;
  entry->ext_status_free = NULL;
}

const char *et_test_get_status_mnemonic(et_test_status status) {
  switch (TEST_STATUS_GET_TYPE(status)) {
    case TEST_SPASS: return "PASS";
    case TEST_SFAIL: return "FAIL";
    case TEST_SSKIPPED:
      if (TEST_STATUS_IS_CLEAN(status)) {
        return "SKI!";
      } else {
        return "SKIP";
      }
    case TEST_SERROR:
      switch (TEST_STATUS_GET_ERROR(status)) {
        case TEST_ENONE: return "ERR0";
        case TEST_EABORT: return "ERRA";
        default: return "ERRP";
      }
    default:
      return "UNKS";
  }
}