| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "charmony.h" |
| |
| #include <errno.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| |
| #define CFC_NEED_BASE_STRUCT_DEF |
| #define CFC_USE_TEST_MACROS |
| #include "CFCTest.h" |
| #include "CFCBase.h" |
| #include "CFCParser.h" |
| #include "CFCUtil.h" |
| |
| typedef struct CFCTestFormatter { |
| void (*batch_prologue)(const CFCTestBatch *batch); |
| void (*vtest_result)(int pass, int test_num, const char *fmt, |
| va_list args); |
| void (*test_comment)(const char *fmt, ...); |
| void (*batch_comment)(const char *fmt, ...); |
| void (*skip)(int test_num, int num_skipped, const char *fmt, va_list args); |
| void (*summary)(const CFCTest *test); |
| } CFCTestFormatter; |
| |
| struct CFCTest { |
| CFCBase base; |
| const CFCTestFormatter *formatter; |
| int num_tests; |
| int num_tests_failed; |
| int num_batches; |
| int num_batches_failed; |
| int num_tests_in_batch; |
| int num_failed_in_batch; |
| }; |
| |
| static int |
| S_do_run_batch(CFCTest *self, const CFCTestBatch *batch); |
| |
| static void |
| S_vtest_true(CFCTest *self, int cond, const char *fmt, va_list args); |
| |
| static void |
| S_format_cfish_batch_prologue(const CFCTestBatch *batch); |
| |
| static void |
| S_format_cfish_vtest_result(int pass, int test_num, const char *fmt, |
| va_list args); |
| |
| static void |
| S_format_cfish_test_comment(const char *fmt, ...); |
| |
| static void |
| S_format_cfish_batch_comment(const char *fmt, ...); |
| |
| static void |
| S_format_cfish_skip(int test_num, int num_skipped, const char *fmt, |
| va_list args); |
| |
| static void |
| S_format_cfish_summary(const CFCTest *test); |
| |
| static void |
| S_format_tap_batch_prologue(const CFCTestBatch *batch); |
| |
| static void |
| S_format_tap_vtest_result(int pass, int test_num, const char *fmt, |
| va_list args); |
| |
| static void |
| S_format_tap_test_comment(const char *fmt, ...); |
| |
| static void |
| S_format_tap_batch_comment(const char *fmt, ...); |
| |
| static void |
| S_format_tap_skip(int test_num, int num_skipped, const char *fmt, |
| va_list args); |
| |
| static void |
| S_format_tap_summary(const CFCTest *test); |
| |
| static const CFCMeta CFCTEST_META = { |
| "Clownfish::CFC::Test", |
| sizeof(CFCTest), |
| (CFCBase_destroy_t)CFCTest_destroy |
| }; |
| |
| static const char *S_test_files_dir; |
| |
| static const CFCTestFormatter S_formatter_cfish = { |
| S_format_cfish_batch_prologue, |
| S_format_cfish_vtest_result, |
| S_format_cfish_test_comment, |
| S_format_cfish_batch_comment, |
| S_format_cfish_skip, |
| S_format_cfish_summary |
| }; |
| |
| static const CFCTestFormatter S_formatter_tap = { |
| S_format_tap_batch_prologue, |
| S_format_tap_vtest_result, |
| S_format_tap_test_comment, |
| S_format_tap_batch_comment, |
| S_format_tap_skip, |
| S_format_tap_summary |
| }; |
| |
| static const CFCTestBatch *const S_batches[] = { |
| &CFCTEST_BATCH_UTIL, |
| &CFCTEST_BATCH_DOCU_COMMENT, |
| &CFCTEST_BATCH_SYMBOL, |
| &CFCTEST_BATCH_VERSION, |
| &CFCTEST_BATCH_TYPE, |
| &CFCTEST_BATCH_FUNCTION, |
| &CFCTEST_BATCH_METHOD, |
| &CFCTEST_BATCH_VARIABLE, |
| &CFCTEST_BATCH_PARAM_LIST, |
| &CFCTEST_BATCH_FILE_SPEC, |
| &CFCTEST_BATCH_CLASS, |
| &CFCTEST_BATCH_C_BLOCK, |
| &CFCTEST_BATCH_PARCEL, |
| &CFCTEST_BATCH_FILE, |
| &CFCTEST_BATCH_HIERARCHY, |
| &CFCTEST_BATCH_PARSER, |
| NULL |
| }; |
| |
| CFCTest* |
| CFCTest_new(const char *formatter_name) { |
| CFCTest *self = (CFCTest*)CFCBase_allocate(&CFCTEST_META); |
| return CFCTest_init(self, formatter_name); |
| } |
| |
| CFCTest* |
| CFCTest_init(CFCTest *self, const char *formatter_name) { |
| if (strcmp(formatter_name, "clownfish") == 0) { |
| self->formatter = &S_formatter_cfish; |
| } |
| else if (strcmp(formatter_name, "tap") == 0) { |
| self->formatter = &S_formatter_tap; |
| } |
| else { |
| CFCUtil_die("Unknown formatter name '%s'", formatter_name); |
| } |
| |
| self->num_tests = 0; |
| self->num_tests_failed = 0; |
| self->num_batches = 0; |
| self->num_batches_failed = 0; |
| self->num_tests_in_batch = 0; |
| self->num_failed_in_batch = 0; |
| |
| return self; |
| } |
| |
| void |
| CFCTest_destroy(CFCTest *self) { |
| CFCBase_destroy((CFCBase*)self); |
| } |
| |
| int |
| CFCTest_run_all(CFCTest *self, const char *test_files_dir) { |
| int failed = 0; |
| |
| S_test_files_dir = test_files_dir; |
| |
| for (int i = 0; S_batches[i]; ++i) { |
| if (!S_do_run_batch(self, S_batches[i])) { failed = 1; } |
| } |
| |
| return !failed; |
| } |
| |
| int |
| CFCTest_run_batch(CFCTest *self, const char *name, |
| const char *test_files_dir) { |
| S_test_files_dir = test_files_dir; |
| |
| for (int i = 0; S_batches[i]; ++i) { |
| const CFCTestBatch *batch = S_batches[i]; |
| if (strcmp(batch->name, name) == 0) { |
| return S_do_run_batch(self, batch); |
| } |
| } |
| |
| CFCUtil_die("Test batch '%s' not found", name); |
| return 0; |
| } |
| |
| static int |
| S_do_run_batch(CFCTest *self, const CFCTestBatch *batch) { |
| self->formatter->batch_prologue(batch); |
| |
| batch->run(self); |
| |
| int failed = 0; |
| void (*comment)(const char *fmt, ...) = self->formatter->batch_comment; |
| |
| if (self->num_failed_in_batch > 0) { |
| failed = 1; |
| comment("%d/%d tests failed.\n", self->num_failed_in_batch, |
| self->num_tests_in_batch); |
| } |
| if (self->num_tests_in_batch != batch->num_planned) { |
| failed = 1; |
| comment("Bad plan: You planned %d tests but ran %d.\n", |
| batch->num_planned, self->num_tests_in_batch); |
| } |
| |
| if (failed) { |
| self->num_batches_failed += 1; |
| } |
| |
| self->num_batches += 1; |
| self->num_tests_in_batch = 0; |
| self->num_failed_in_batch = 0; |
| |
| return !failed; |
| } |
| |
| char* |
| CFCTest_path(const char *path) { |
| const char *dir = S_test_files_dir |
| ? S_test_files_dir |
| : ".." CHY_DIR_SEP "common" CHY_DIR_SEP "test"; |
| |
| return CFCUtil_sprintf("%s" CHY_DIR_SEP "%s", dir, path); |
| } |
| |
| void |
| CFCTest_test_true(CFCTest *self, int cond, const char *fmt, ...) { |
| va_list args; |
| va_start(args, fmt); |
| S_vtest_true(self, cond, fmt, args); |
| va_end(args); |
| } |
| |
| void |
| CFCTest_test_string_equals(CFCTest *self, const char *result, |
| const char *expected, const char *fmt, ...) { |
| int cond = (strcmp(result, expected) == 0); |
| |
| va_list args; |
| va_start(args, fmt); |
| S_vtest_true(self, cond, fmt, args); |
| va_end(args); |
| |
| if (!cond) { |
| self->formatter->test_comment("Expected '%s', got '%s'.\n", |
| expected, result); |
| } |
| } |
| |
| void |
| CFCTest_test_int_equals(CFCTest *self, int64_t result, int64_t expected, |
| const char *fmt, ...) { |
| int cond = (result == expected); |
| |
| va_list args; |
| va_start(args, fmt); |
| S_vtest_true(self, cond, fmt, args); |
| va_end(args); |
| |
| if (!cond) { |
| self->formatter->test_comment("Expected '%"PRId64"'," |
| " got '%"PRId64"'.\n", |
| expected, result); |
| } |
| } |
| |
| void |
| CFCTest_test_uint_equals(CFCTest *self, uint64_t result, uint64_t expected, |
| const char *fmt, ...) { |
| int cond = (result == expected); |
| |
| va_list args; |
| va_start(args, fmt); |
| S_vtest_true(self, cond, fmt, args); |
| va_end(args); |
| |
| if (!cond) { |
| self->formatter->test_comment("Expected '%"PRIu64"'," |
| " got '%"PRIu64"'.\n", |
| expected, result); |
| } |
| } |
| |
| void |
| CFCTest_skip(CFCTest *self, int num, const char *fmt, ...) { |
| va_list args; |
| va_start(args, fmt); |
| self->formatter->skip(self->num_tests + 1, num, fmt, args); |
| va_end(args); |
| |
| self->num_tests += num; |
| self->num_tests_in_batch += num; |
| } |
| |
| int |
| CFCTest_finish(CFCTest *self) { |
| self->formatter->summary(self); |
| |
| return self->num_batches != 0 && self->num_batches_failed == 0; |
| } |
| |
| static void |
| S_vtest_true(CFCTest *self, int cond, const char *fmt, va_list args) { |
| self->num_tests += 1; |
| self->num_tests_in_batch += 1; |
| |
| if (!cond) { |
| self->num_tests_failed += 1; |
| self->num_failed_in_batch += 1; |
| } |
| |
| self->formatter->vtest_result(cond, self->num_tests_in_batch, fmt, args); |
| } |
| |
| static void |
| S_format_cfish_batch_prologue(const CFCTestBatch *batch) { |
| printf("Testing %s...\n", batch->name); |
| } |
| |
| static void |
| S_format_cfish_vtest_result(int pass, int test_num, const char *fmt, |
| va_list args) { |
| if (!pass) { |
| printf(" Failed test %d: ", test_num); |
| vprintf(fmt, args); |
| printf("\n"); |
| } |
| else if (getenv("CFCTEST_VERBOSE")) { |
| printf(" Passed test %d: ", test_num); |
| vprintf(fmt, args); |
| printf("\n"); |
| } |
| } |
| |
| static void |
| S_format_cfish_test_comment(const char *fmt, ...) { |
| printf(" "); |
| |
| va_list args; |
| va_start(args, fmt); |
| vprintf(fmt, args); |
| va_end(args); |
| } |
| |
| static void |
| S_format_cfish_batch_comment(const char *fmt, ...) { |
| printf(" "); |
| |
| va_list args; |
| va_start(args, fmt); |
| vprintf(fmt, args); |
| va_end(args); |
| } |
| |
| static void |
| S_format_cfish_skip(int test_num, int num_skipped, const char *fmt, |
| va_list args) { |
| CHY_UNUSED_VAR(test_num); |
| CHY_UNUSED_VAR(num_skipped); |
| CHY_UNUSED_VAR(fmt); |
| CHY_UNUSED_VAR(args); |
| } |
| |
| static void |
| S_format_cfish_summary(const CFCTest *test) { |
| if (test->num_batches == 0) { |
| printf("No tests planned or run.\n"); |
| } |
| else if (test->num_batches_failed == 0) { |
| printf("%d batches passed. %d tests passed.\n", |
| test->num_batches, test->num_tests); |
| printf("Result: PASS\n"); |
| } |
| else { |
| printf("%d/%d batches failed. %d/%d tests failed.\n", |
| test->num_batches_failed, test->num_batches, |
| test->num_tests_failed, test->num_tests); |
| printf("Result: FAIL\n"); |
| } |
| } |
| |
| static void |
| S_format_tap_batch_prologue(const CFCTestBatch *batch) { |
| printf("1..%d\n", batch->num_planned); |
| } |
| |
| static void |
| S_format_tap_vtest_result(int pass, int test_num, const char *fmt, |
| va_list args) { |
| const char *result = pass ? "ok" : "not ok"; |
| printf("%s %d - ", result, test_num); |
| vprintf(fmt, args); |
| printf("\n"); |
| } |
| |
| static void |
| S_format_tap_test_comment(const char *fmt, ...) { |
| printf("# "); |
| |
| va_list args; |
| va_start(args, fmt); |
| vprintf(fmt, args); |
| va_end(args); |
| } |
| |
| static void |
| S_format_tap_batch_comment(const char *fmt, ...) { |
| printf("# "); |
| |
| va_list args; |
| va_start(args, fmt); |
| vprintf(fmt, args); |
| va_end(args); |
| } |
| |
| static void |
| S_format_tap_skip(int test_num, int num_skipped, const char *fmt, |
| va_list args) { |
| for (int i = 0; i < num_skipped; ++i) { |
| printf("ok %d # SKIP ", test_num + i); |
| vprintf(fmt, args); |
| printf("\n"); |
| } |
| } |
| |
| static void |
| S_format_tap_summary(const CFCTest *test) { |
| (void)test; // unused |
| } |
| |
| struct CFCParcel* |
| CFCTest_parse_parcel(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Parcel", "result class of '%s'", src); |
| return (struct CFCParcel*)result; |
| } |
| |
| struct CFCType* |
| CFCTest_parse_type(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Type", "result class of '%s'", src); |
| return (struct CFCType*)result; |
| } |
| |
| struct CFCVariable* |
| CFCTest_parse_variable(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Variable", "result class of '%s'", src); |
| return (struct CFCVariable*)result; |
| } |
| |
| struct CFCParamList* |
| CFCTest_parse_param_list(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::ParamList", "result class of '%s'", src); |
| return (struct CFCParamList*)result; |
| } |
| |
| struct CFCFunction* |
| CFCTest_parse_function(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Function", "result class of '%s'", src); |
| return (struct CFCFunction*)result; |
| } |
| |
| struct CFCMethod* |
| CFCTest_parse_method(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse '%s'", src); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Method", "result class of '%s'", src); |
| return (struct CFCMethod*)result; |
| } |
| |
| struct CFCClass* |
| CFCTest_parse_class(CFCTest *test, CFCParser *parser, const char *src) { |
| CFCBase *result = CFCParser_parse(parser, src); |
| OK(test, result != NULL, "parse class"); |
| STR_EQ(test, CFCBase_get_cfc_class(result), |
| "Clownfish::CFC::Model::Class", "result class"); |
| return (struct CFCClass*)result; |
| } |
| |
| time_t |
| CFCTest_get_file_mtime(const char *path) { |
| struct stat buf; |
| if (stat(path, &buf)) { |
| CFCUtil_die("Can't stat '%s': %s", path, strerror(errno)); |
| } |
| return buf.st_mtime; |
| } |
| |
| #if defined(CHY_HAS_WINDOWS_H) |
| |
| #include <windows.h> |
| |
| void |
| CFCTest_set_file_times(const char *path, time_t time) { |
| // Strawberry Perl may unpack the distribution's files as read-only. |
| DWORD attrs = GetFileAttributes(path); |
| if (attrs == INVALID_FILE_ATTRIBUTES) { |
| CFCUtil_die("Can't get file attrs of '%s': %u", path, GetLastError()); |
| } |
| if (attrs & FILE_ATTRIBUTE_READONLY) { |
| attrs &= ~FILE_ATTRIBUTE_READONLY; |
| if (!SetFileAttributes(path, attrs)) { |
| CFCUtil_die("Can't make '%s' writable: %u", path, |
| GetLastError()); |
| } |
| } |
| |
| HANDLE handle = CreateFile(path, GENERIC_WRITE, FILE_SHARE_READ, NULL, |
| OPEN_EXISTING, 0, NULL); |
| if (handle == INVALID_HANDLE_VALUE) { |
| CFCUtil_die("Can't open '%s': %u", path, GetLastError()); |
| } |
| uint64_t ticks = 10000000 * (UINT64_C(11644473600) + time); |
| FILETIME file_time; |
| file_time.dwLowDateTime = (DWORD)ticks; |
| file_time.dwHighDateTime = (DWORD)(ticks >> 32); |
| if (!SetFileTime(handle, &file_time, &file_time, &file_time)) { |
| CFCUtil_die("Can't set file time of '%s': %u", path, GetLastError()); |
| } |
| CloseHandle(handle); |
| } |
| |
| #elif defined(CHY_HAS_UTIME_H) |
| |
| #include <utime.h> |
| |
| void |
| CFCTest_set_file_times(const char *path, time_t time) { |
| struct utimbuf buf; |
| buf.actime = time; |
| buf.modtime = time; |
| if (utime(path, &buf)) { |
| CFCUtil_die("Can't set file time of '%s': %s", path, strerror(errno)); |
| } |
| } |
| |
| #else |
| |
| #error Need either utime.h or windows.h |
| |
| #endif |
| |