diff --git a/lib/cmp_icu_new.c b/lib/cmp_icu_new.c new file mode 100644 index 0000000000000000000000000000000000000000..e6f6752e6297f68c8cae3971066191cb8ae3ca9b --- /dev/null +++ b/lib/cmp_icu_new.c @@ -0,0 +1,165 @@ +/** + * @file icu_cmp.c + * @author Dominik Loidolt (dominik.loidolt@univie.ac.at), + * @date 2020 + * + * @copyright GPLv2 + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * @brief software compression library + * @see Data Compression User Manual PLATO-UVIE-PL-UM-0001 + */ + + +#include <stdint.h> + +#include "cmp_debug.h" + + +/* return code if the bitstream buffer is too small to store the whole bitstream */ +#define CMP_ERROR_SAMLL_BUF -2 + + +/** + * @brief put the value of up to 32 bits into a bitstream accessed as 32-bit + * RAM in big-endian + * + * @param value the value to put + * @param n_bits number of bits to put in the bitstream + * @param bit_offset bit index where the bits will be put, seen from the very + * beginning of the bitstream + * @param bitstream_adr this is the pointer to the beginning of the bitstream + * (can be NULL) + * @param max_bit_len maximum length of the bitstream in bits; is ignored if + * bitstream_adr is NULL + * + * @returns length in bits of the generated bitstream on success; returns + * negative in case of erroneous input; returns CMP_ERROR_SAMLL_BUF if + * the bitstream buffer is too small to put the value in the bitstream + * @note a value with more bits set as the n_bits parameter is considered as an + * erroneous input. + */ + +static int put_n_bits32(uint32_t value, unsigned int n_bits, int bit_offset, + uint32_t *bitstream_adr, unsigned int max_bit_len) +{ + uint32_t *local_adr; + uint32_t mask; + unsigned int shiftRight, shiftLeft, bitsLeft, bitsRight; + int stream_len = (int)(n_bits + (unsigned int)bit_offset); /* overflow results in a negative return value */ + + /* leave in case of erroneous input */ + if (bit_offset < 0) + return -1; + + if (n_bits == 0) + return 0; + + if (n_bits > 32) + return -1; + + /* (M) is the n_bits parameter large enough to cover all value bits; the + * calculations can be re-used in the unsegmented code, so we have no overhead + */ + shiftRight = 32 - n_bits; + mask = 0xFFFFFFFFU >> shiftRight; + if (value & ~mask) { + debug_print("Error: Not all set bits in the put value are added to the bitstream. Check value n_bits parameter combination.\n"); + return -1; + } + + /* Do we need to write data to the bitstream? */ + if (!bitstream_adr) + return stream_len; + + /* Check if bitstream buffer is large enough */ + if ((unsigned int)stream_len > max_bit_len) { + debug_print("Error: The buffer for the compressed data is too small to hold the compressed data. Try a larger buffer_length parameter.\n"); + return CMP_ERROR_SAMLL_BUF; + } + + /* Separate the bit_offset into word offset (set local_adr pointer) and local bit offset (bitsLeft) */ + local_adr = bitstream_adr + (bit_offset >> 5); + bitsLeft = bit_offset & 0x1F; + + /* Calculate the bitsRight for the unsegmented case. If bitsRight is + * negative we need to split the value over two words + */ + bitsRight = shiftRight - bitsLeft; + + if ((int)bitsRight >= 0) { + /* UNSEGMENTED + * + *|-----------|XXXXX|----------------| + * bitsLeft n bitsRight + * + * -> to get the mask: + * shiftRight = bitsLeft + bitsRight = 32 - n + * shiftLeft = bitsRight = 32 - n - bitsLeft = shiftRight - bitsLeft + */ + + shiftLeft = bitsRight; + + /* generate the mask, the bits for the values will be true + * shiftRight = 32 - n_bits; see (M) above! + * mask = (0XFFFFFFFF >> shiftRight) << shiftLeft; see (M) above! + */ + mask <<= shiftLeft; + value <<= shiftLeft; + + /* clear the destination with inverse mask */ + *(local_adr) &= ~mask; + + /* assign the value */ + *(local_adr) |= value; + + } else { + /* SEGMENTED + * + *|-----------------------------|XXX| |XX|------------------------------| + * bitsLeft n1 n2 bitsRight + * + * -> to get the mask part 1: + * shiftRight = bitsLeft + * n1 = n - (bitsLeft + n - 32) = 32 - bitsLeft + * + * -> to get the mask part 2: + * n2 = bitsLeft + n - 32 = -(32 - n - bitsLeft) = -(bitsRight_UNSEGMENTED) + * shiftLeft = 32 - n2 = 32 - (bitsLeft + n - 32) = 64 - bitsLeft - n + * + */ + + unsigned int n2 = -bitsRight; + + /* part 1: */ + shiftRight = bitsLeft; + mask = 0XFFFFFFFFU >> shiftRight; + + /* clear the destination with inverse mask */ + *(local_adr) &= ~mask; + + /* assign the value part 1 */ + *(local_adr) |= (value >> n2); + + /* part 2: */ + /* adjust address */ + local_adr += 1; + shiftLeft = 32 - n2; + mask = 0XFFFFFFFFU << shiftLeft; + + /* clear the destination with inverse mask */ + *(local_adr) &= ~mask; + + /* assign the value part 2 */ + *(local_adr) |= (value << shiftLeft); + } + return stream_len; +} + diff --git a/subprojects/unity.wrap b/subprojects/unity.wrap new file mode 100644 index 0000000000000000000000000000000000000000..8cc488366197ec4f255d93fb4642367c88f5744b --- /dev/null +++ b/subprojects/unity.wrap @@ -0,0 +1,8 @@ +[wrap-file] +directory = Unity-2.5.2 +source_url = https://github.com/ThrowTheSwitch/Unity/archive/refs/tags/v2.5.2.tar.gz +source_filename = Unity-2.5.2.tar.gz +source_hash = 3786de6c8f389be3894feae4f7d8680a02e70ed4dbcce36109c8f8646da2671a + +[provide] +unity = unity_dep diff --git a/test/cmp_icu/test_cmp_icu_new.c b/test/cmp_icu/test_cmp_icu_new.c new file mode 100644 index 0000000000000000000000000000000000000000..6e5761528a50305999b110a74120a05f70508e01 --- /dev/null +++ b/test/cmp_icu/test_cmp_icu_new.c @@ -0,0 +1,373 @@ +#include <string.h> + +#include "unity.h" + +/* this is a hack to test static functions */ +#include "../lib/cmp_icu_new.c" + + + +/** + * @test put_n_bits32 + */ + +#define SDP_PB_N 3 + + +static void init_PB32_arrays(uint32_t *z, uint32_t *o) +{ + uint32_t i; + + /* init testarray with all 0 and all 1 */ + for (i = 0; i < SDP_PB_N; i++) { + z[i] = 0; + o[i] = 0xffffffff; + } +} + + +void test_put_n_bits32(void) +{ + uint32_t v, n; + int o, rval; /* return value */ + uint32_t testarray0[SDP_PB_N]; + uint32_t testarray1[SDP_PB_N]; + const uint32_t l = sizeof(testarray0) * CHAR_BIT; + + /* hereafter, the value is v, + * the number of bits to write is n, + * the offset of the bit is o, + * the max length the bitstream in bits is l + */ + + init_PB32_arrays(testarray0, testarray1); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /*** n=0 ***/ + + /* do not write, left border */ + v = 0; n = 0; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /* TODO: not a valid test */ + v = 0xffffffff; n = 0; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /* do not write, right border */ + v = 0; n = 0; o = 31; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /* TODO: not a valid test */ + /* test value = 0xffffffff; N = 0 */ + v = 0xffffffff; n = 0; o = 31; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(0, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /*** n=1 ***/ + + /* left border, write 0 */ + v = 0; n = 1; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(1, rval); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(1, rval); + TEST_ASSERT(testarray1[0] == 0x7fffffff); + + /* left border, write 1 */ + v = 1; n = 1; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(1, rval); + TEST_ASSERT(testarray0[0] == 0x80000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(1, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + + /* left border, write 32 */ + v = 0xf0f0abcd; n = 32; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray0[0] == 0xf0f0abcd); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray1[0] == 0xf0f0abcd); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* middle, write 2 bits */ + v = 3; n = 2; o = 29; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 31); + TEST_ASSERT(testarray0[0] == 0x6); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT_EQUAL_INT(rval, 31); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /*** n=5, unsegmented ***/ + + /* left border, write 0 */ + v = 0; n = 5; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 5); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT(testarray1[0] == 0x07ffffff); + TEST_ASSERT_EQUAL_INT(rval, 5); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* left border, write 11111 */ + v = 0x1f; n = 5; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 5); + TEST_ASSERT(testarray0[0] == 0xf8000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 5); + TEST_ASSERT(testarray1[0] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* middle, write 0 */ + v = 0; n = 5; o = 7; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 12); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 12); + TEST_ASSERT(testarray1[0] == 0xfe0fffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* middle, write 11111 */ + v = 0x1f; n = 5; o = 7; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 12); + TEST_ASSERT(testarray0[0] == 0x01f00000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 12); + TEST_ASSERT(testarray1[0] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* right, write 0 */ + v = 0; n = 5; o = 91; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 96); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + TEST_ASSERT(testarray0[0] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 96); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xffffffff); + TEST_ASSERT(testarray1[2] == 0xffffffe0); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* right, write 11111 */ + v = 0x1f; n = 5; o = 91; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 96); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + TEST_ASSERT(testarray0[2] == 0x0000001f); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 96); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xffffffff); + TEST_ASSERT(testarray1[2] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* 32 bit, write 0 */ + v = 0; n = 32; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray0[0] == 0x00000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray1[0] == 0x00000000); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* 32 bit, write -1 */ + v = 0xffffffff; n = 32; o = 0; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray0[0] == 0xffffffff); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 32); + TEST_ASSERT(testarray1[0] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* SEGMENTED cases */ + /* 5 bit, write 0 */ + v = 0; n = 5; o = 62; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 67); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + TEST_ASSERT(testarray0[2] == 0); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 67); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xfffffffc); + TEST_ASSERT(testarray1[2] == 0x1fffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* 5 bit, write 1f */ + v = 0x1f; n = 5; o = 62; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 67); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 3); + TEST_ASSERT(testarray0[2] == 0xe0000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 67); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xffffffff); + TEST_ASSERT(testarray1[2] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* 32 bit, write 0 */ + v = 0; n = 32; o = 1; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 33); + TEST_ASSERT(testarray0[0] == 0x00000000); + TEST_ASSERT(testarray0[1] == 0x00000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 33); + TEST_ASSERT(testarray1[0] == 0x80000000); + TEST_ASSERT(testarray1[1] == 0x7fffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* 32 bit, write -1 */ + v = 0xffffffff; n = 32; o = 1; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, 33); + TEST_ASSERT(testarray0[0] == 0x7fffffff); + TEST_ASSERT(testarray0[1] == 0x80000000); + + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(rval, 33); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xffffffff); + /* re-init input arrays after clobbering */ + init_PB32_arrays(testarray0, testarray1); + + /* test NULL buffer */ + v = 0; n = 0; o = 0; + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(rval, 0); + + v = 0; n = 1; o = 0; + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(rval, 1); + + v = 0; n = 5; o = 31; + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(rval, 36); + + v = 0; n = 2; o = 95; + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(rval, 97); /* rval can be longer than l */ + + /* error cases */ + /* n too large */ + v = 0x0; n = 33; o = 1; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(rval, -1); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(rval, -1); + + /* value larger than n allows */ + v = 0x7f; n = 6; o = 10; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(-1, rval); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(-1, rval); + + /* try to put too much in the bitstream */ + v = 0x1; n = 1; o = 96; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(CMP_ERROR_SAMLL_BUF, rval); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + TEST_ASSERT(testarray0[2] == 0); + + /* this should work */ + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(97, rval); + + /* offset lager than max_bit_len */ + v = 0x0; n = 32; o = INT32_MAX; + rval = put_n_bits32(v, n, o, testarray1, l); + TEST_ASSERT_EQUAL_INT(CMP_ERROR_SAMLL_BUF, rval); + TEST_ASSERT(testarray1[0] == 0xffffffff); + TEST_ASSERT(testarray1[1] == 0xffffffff); + TEST_ASSERT(testarray1[2] == 0xffffffff); + + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT(rval < 0); + + /* negative offset */ + v = 0x0; n = 0; o = -1; + rval = put_n_bits32(v, n, o, testarray0, l); + TEST_ASSERT_EQUAL_INT(-1, rval); + TEST_ASSERT(testarray0[0] == 0); + TEST_ASSERT(testarray0[1] == 0); + + rval = put_n_bits32(v, n, o, NULL, l); + TEST_ASSERT_EQUAL_INT(-1, rval); +} diff --git a/test/tools/generate_test_runner.rb b/test/tools/generate_test_runner.rb new file mode 100755 index 0000000000000000000000000000000000000000..1c0ec3450435f7c8cf95219afd6f9226b7389a0f --- /dev/null +++ b/test/tools/generate_test_runner.rb @@ -0,0 +1,512 @@ +#!/usr/bin/env ruby +# ========================================== +# Unity Project - A Test Framework for C +# Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams +# [Released under MIT License. Please refer to license.txt for details] +# ========================================== + +class UnityTestRunnerGenerator + def initialize(options = nil) + @options = UnityTestRunnerGenerator.default_options + case options + when NilClass + @options + when String + @options.merge!(UnityTestRunnerGenerator.grab_config(options)) + when Hash + # Check if some of these have been specified + @options[:has_setup] = !options[:setup_name].nil? + @options[:has_teardown] = !options[:teardown_name].nil? + @options[:has_suite_setup] = !options[:suite_setup].nil? + @options[:has_suite_teardown] = !options[:suite_teardown].nil? + @options.merge!(options) + else + raise 'If you specify arguments, it should be a filename or a hash of options' + end + require_relative 'type_sanitizer' + end + + def self.default_options + { + includes: [], + defines: [], + plugins: [], + framework: :unity, + test_prefix: 'test|spec|should', + mock_prefix: 'Mock', + mock_suffix: '', + setup_name: 'setUp', + teardown_name: 'tearDown', + test_reset_name: 'resetTest', + test_verify_name: 'verifyTest', + main_name: 'main', # set to :auto to automatically generate each time + main_export_decl: '', + cmdline_args: false, + omit_begin_end: false, + use_param_tests: false, + include_extensions: '(?:hpp|hh|H|h)', + source_extensions: '(?:cpp|cc|ino|C|c)' + } + end + + def self.grab_config(config_file) + options = default_options + unless config_file.nil? || config_file.empty? + require 'yaml' + yaml_guts = YAML.load_file(config_file) + options.merge!(yaml_guts[:unity] || yaml_guts[:cmock]) + raise "No :unity or :cmock section found in #{config_file}" unless options + end + options + end + + def run(input_file, output_file, options = nil) + @options.merge!(options) unless options.nil? + + # pull required data from source file + source = File.read(input_file) + source = source.force_encoding('ISO-8859-1').encode('utf-8', replace: nil) + tests = find_tests(source) + headers = find_includes(source) + testfile_includes = (headers[:local] + headers[:system]) + used_mocks = find_mocks(testfile_includes) + testfile_includes = (testfile_includes - used_mocks) + testfile_includes.delete_if { |inc| inc =~ /(unity|cmock)/ } + find_setup_and_teardown(source) + + # build runner file + generate(input_file, output_file, tests, used_mocks, testfile_includes) + + # determine which files were used to return them + all_files_used = [input_file, output_file] + all_files_used += testfile_includes.map { |filename| filename + '.c' } unless testfile_includes.empty? + all_files_used += @options[:includes] unless @options[:includes].empty? + all_files_used += headers[:linkonly] unless headers[:linkonly].empty? + all_files_used.uniq + end + + def generate(input_file, output_file, tests, used_mocks, testfile_includes) + File.open(output_file, 'w') do |output| + create_header(output, used_mocks, testfile_includes) + create_externs(output, tests, used_mocks) + create_mock_management(output, used_mocks) + create_setup(output) + create_teardown(output) + create_suite_setup(output) + create_suite_teardown(output) + create_reset(output) + create_run_test(output) unless tests.empty? + create_args_wrappers(output, tests) + create_main(output, input_file, tests, used_mocks) + end + + return unless @options[:header_file] && !@options[:header_file].empty? + + File.open(@options[:header_file], 'w') do |output| + create_h_file(output, @options[:header_file], tests, testfile_includes, used_mocks) + end + end + + def find_tests(source) + tests_and_line_numbers = [] + + # contains characters which will be substituted from within strings, doing + # this prevents these characters from interfering with scrubbers + # @ is not a valid C character, so there should be no clashes with files genuinely containing these markers + substring_subs = { '{' => '@co@', '}' => '@cc@', ';' => '@ss@', '/' => '@fs@' } + substring_re = Regexp.union(substring_subs.keys) + substring_unsubs = substring_subs.invert # the inverse map will be used to fix the strings afterwords + substring_unsubs['@quote@'] = '\\"' + substring_unsubs['@apos@'] = '\\\'' + substring_unre = Regexp.union(substring_unsubs.keys) + source_scrubbed = source.clone + source_scrubbed = source_scrubbed.gsub(/\\"/, '@quote@') # hide escaped quotes to allow capture of the full string/char + source_scrubbed = source_scrubbed.gsub(/\\'/, '@apos@') # hide escaped apostrophes to allow capture of the full string/char + source_scrubbed = source_scrubbed.gsub(/("[^"\n]*")|('[^'\n]*')/) { |s| s.gsub(substring_re, substring_subs) } # temporarily hide problematic characters within strings + source_scrubbed = source_scrubbed.gsub(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks + source_scrubbed = source_scrubbed.gsub(/\/\*.*?\*\//m, '') # remove block comments + source_scrubbed = source_scrubbed.gsub(/\/\/.*$/, '') # remove line comments (all that remain) + lines = source_scrubbed.split(/(^\s*\#.*$) | (;|\{|\}) /x) # Treat preprocessor directives as a logical line. Match ;, {, and } as end of lines + .map { |line| line.gsub(substring_unre, substring_unsubs) } # unhide the problematic characters previously removed + + lines.each_with_index do |line, _index| + # find tests + next unless line =~ /^((?:\s*(?:TEST_CASE|TEST_RANGE)\s*\(.*?\)\s*)*)\s*void\s+((?:#{@options[:test_prefix]}).*)\s*\(\s*(.*)\s*\)/m + + arguments = Regexp.last_match(1) + name = Regexp.last_match(2) + call = Regexp.last_match(3) + params = Regexp.last_match(4) + args = nil + + if @options[:use_param_tests] && !arguments.empty? + args = [] + arguments.scan(/\s*TEST_CASE\s*\((.*)\)\s*$/) { |a| args << a[0] } + + arguments.scan(/\s*TEST_RANGE\s*\((.*)\)\s*$/).flatten.each do |range_str| + args += range_str.scan(/\[(-?\d+.?\d*), *(-?\d+.?\d*), *(-?\d+.?\d*)\]/).map do |arg_values_str| + arg_values_str.map do |arg_value_str| + arg_value_str.include?('.') ? arg_value_str.to_f : arg_value_str.to_i + end + end.map do |arg_values| + (arg_values[0]..arg_values[1]).step(arg_values[2]).to_a + end.reduce do |result, arg_range_expanded| + result.product(arg_range_expanded) + end.map do |arg_combinations| + arg_combinations.flatten.join(', ') + end + end + end + + tests_and_line_numbers << { test: name, args: args, call: call, params: params, line_number: 0 } + end + + tests_and_line_numbers.uniq! { |v| v[:test] } + + # determine line numbers and create tests to run + source_lines = source.split("\n") + source_index = 0 + tests_and_line_numbers.size.times do |i| + source_lines[source_index..-1].each_with_index do |line, index| + next unless line =~ /\s+#{tests_and_line_numbers[i][:test]}(?:\s|\()/ + + source_index += index + tests_and_line_numbers[i][:line_number] = source_index + 1 + break + end + end + + tests_and_line_numbers + end + + def find_includes(source) + # remove comments (block and line, in three steps to ensure correct precedence) + source.gsub!(/\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks + source.gsub!(/\/\*.*?\*\//m, '') # remove block comments + source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain) + + # parse out includes + includes = { + local: source.scan(/^\s*#include\s+\"\s*(.+\.#{@options[:include_extensions]})\s*\"/).flatten, + system: source.scan(/^\s*#include\s+<\s*(.+)\s*>/).flatten.map { |inc| "<#{inc}>" }, + linkonly: source.scan(/^TEST_FILE\(\s*\"\s*(.+\.#{@options[:source_extensions]})\s*\"/).flatten + } + includes + end + + def find_mocks(includes) + mock_headers = [] + includes.each do |include_path| + include_file = File.basename(include_path) + mock_headers << include_path if include_file =~ /^#{@options[:mock_prefix]}.*#{@options[:mock_suffix]}\.h$/i + end + mock_headers + end + + def find_setup_and_teardown(source) + @options[:has_setup] = source =~ /void\s+#{@options[:setup_name]}\s*\(/ + @options[:has_teardown] = source =~ /void\s+#{@options[:teardown_name]}\s*\(/ + @options[:has_suite_setup] ||= (source =~ /void\s+suiteSetUp\s*\(/) + @options[:has_suite_teardown] ||= (source =~ /int\s+suiteTearDown\s*\(int\s+([a-zA-Z0-9_])+\s*\)/) + end + + def create_header(output, mocks, testfile_includes = []) + output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */') + output.puts("\n/*=======Automagically Detected Files To Include=====*/") + output.puts("#include \"#{@options[:framework]}.h\"") + output.puts('#include "cmock.h"') unless mocks.empty? + if @options[:defines] && !@options[:defines].empty? + @options[:defines].each { |d| output.puts("#ifndef #{d}\n#define #{d}\n#endif /* #{d} */") } + end + if @options[:header_file] && !@options[:header_file].empty? + output.puts("#include \"#{File.basename(@options[:header_file])}\"") + else + @options[:includes].flatten.uniq.compact.each do |inc| + output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}") + end + testfile_includes.each do |inc| + output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}") + end + end + mocks.each do |mock| + output.puts("#include \"#{mock}\"") + end + output.puts('#include "CException.h"') if @options[:plugins].include?(:cexception) + + return unless @options[:enforce_strict_ordering] + + output.puts('') + output.puts('int GlobalExpectCount;') + output.puts('int GlobalVerifyOrder;') + output.puts('char* GlobalOrderError;') + end + + def create_externs(output, tests, _mocks) + output.puts("\n/*=======External Functions This Runner Calls=====*/") + output.puts("extern void #{@options[:setup_name]}(void);") + output.puts("extern void #{@options[:teardown_name]}(void);") + output.puts("\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif") if @options[:externc] + tests.each do |test| + output.puts("extern void #{test[:test]}(#{test[:call] || 'void'});") + end + output.puts("#ifdef __cplusplus\n}\n#endif") if @options[:externc] + output.puts('') + end + + def create_mock_management(output, mock_headers) + output.puts("\n/*=======Mock Management=====*/") + output.puts('static void CMock_Init(void)') + output.puts('{') + + if @options[:enforce_strict_ordering] + output.puts(' GlobalExpectCount = 0;') + output.puts(' GlobalVerifyOrder = 0;') + output.puts(' GlobalOrderError = NULL;') + end + + mocks = mock_headers.map { |mock| File.basename(mock, '.*') } + mocks.each do |mock| + mock_clean = TypeSanitizer.sanitize_c_identifier(mock) + output.puts(" #{mock_clean}_Init();") + end + output.puts("}\n") + + output.puts('static void CMock_Verify(void)') + output.puts('{') + mocks.each do |mock| + mock_clean = TypeSanitizer.sanitize_c_identifier(mock) + output.puts(" #{mock_clean}_Verify();") + end + output.puts("}\n") + + output.puts('static void CMock_Destroy(void)') + output.puts('{') + mocks.each do |mock| + mock_clean = TypeSanitizer.sanitize_c_identifier(mock) + output.puts(" #{mock_clean}_Destroy();") + end + output.puts("}\n") + end + + def create_setup(output) + return if @options[:has_setup] + + output.puts("\n/*=======Setup (stub)=====*/") + output.puts("void #{@options[:setup_name]}(void) {}") + end + + def create_teardown(output) + return if @options[:has_teardown] + + output.puts("\n/*=======Teardown (stub)=====*/") + output.puts("void #{@options[:teardown_name]}(void) {}") + end + + def create_suite_setup(output) + return if @options[:suite_setup].nil? + + output.puts("\n/*=======Suite Setup=====*/") + output.puts('void suiteSetUp(void)') + output.puts('{') + output.puts(@options[:suite_setup]) + output.puts('}') + end + + def create_suite_teardown(output) + return if @options[:suite_teardown].nil? + + output.puts("\n/*=======Suite Teardown=====*/") + output.puts('int suiteTearDown(int num_failures)') + output.puts('{') + output.puts(@options[:suite_teardown]) + output.puts('}') + end + + def create_reset(output) + output.puts("\n/*=======Test Reset Options=====*/") + output.puts("void #{@options[:test_reset_name]}(void);") + output.puts("void #{@options[:test_reset_name]}(void)") + output.puts('{') + output.puts(" #{@options[:teardown_name]}();") + output.puts(' CMock_Verify();') + output.puts(' CMock_Destroy();') + output.puts(' CMock_Init();') + output.puts(" #{@options[:setup_name]}();") + output.puts('}') + output.puts("void #{@options[:test_verify_name]}(void);") + output.puts("void #{@options[:test_verify_name]}(void)") + output.puts('{') + output.puts(' CMock_Verify();') + output.puts('}') + end + + def create_run_test(output) + require 'erb' + template = ERB.new(File.read(File.join(__dir__, 'run_test.erb')), nil, '<>') + output.puts("\n" + template.result(binding)) + end + + def create_args_wrappers(output, tests) + return unless @options[:use_param_tests] + + output.puts("\n/*=======Parameterized Test Wrappers=====*/") + tests.each do |test| + next if test[:args].nil? || test[:args].empty? + + test[:args].each.with_index(1) do |args, idx| + output.puts("static void runner_args#{idx}_#{test[:test]}(void)") + output.puts('{') + output.puts(" #{test[:test]}(#{args});") + output.puts("}\n") + end + end + end + + def create_main(output, filename, tests, used_mocks) + output.puts("\n/*=======MAIN=====*/") + main_name = @options[:main_name].to_sym == :auto ? "main_#{filename.gsub('.c', '')}" : (@options[:main_name]).to_s + if @options[:cmdline_args] + if main_name != 'main' + output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv);") + end + output.puts("#{@options[:main_export_decl]} int #{main_name}(int argc, char** argv)") + output.puts('{') + output.puts(' int parse_status = UnityParseOptions(argc, argv);') + output.puts(' if (parse_status != 0)') + output.puts(' {') + output.puts(' if (parse_status < 0)') + output.puts(' {') + output.puts(" UnityPrint(\"#{filename.gsub('.c', '')}.\");") + output.puts(' UNITY_PRINT_EOL();') + tests.each do |test| + if (!@options[:use_param_tests]) || test[:args].nil? || test[:args].empty? + output.puts(" UnityPrint(\" #{test[:test]}\");") + output.puts(' UNITY_PRINT_EOL();') + else + test[:args].each do |args| + output.puts(" UnityPrint(\" #{test[:test]}(#{args})\");") + output.puts(' UNITY_PRINT_EOL();') + end + end + end + output.puts(' return 0;') + output.puts(' }') + output.puts(' return parse_status;') + output.puts(' }') + else + main_return = @options[:omit_begin_end] ? 'void' : 'int' + if main_name != 'main' + output.puts("#{@options[:main_export_decl]} #{main_return} #{main_name}(void);") + end + output.puts("#{main_return} #{main_name}(void)") + output.puts('{') + end + output.puts(' suiteSetUp();') if @options[:has_suite_setup] + if @options[:omit_begin_end] + output.puts(" UnitySetTestFile(\"#{filename.gsub(/\\/, '\\\\\\')}\");") + else + output.puts(" UnityBegin(\"#{filename.gsub(/\\/, '\\\\\\')}\");") + end + tests.each do |test| + if (!@options[:use_param_tests]) || test[:args].nil? || test[:args].empty? + output.puts(" run_test(#{test[:test]}, \"#{test[:test]}\", #{test[:line_number]});") + else + test[:args].each.with_index(1) do |args, idx| + wrapper = "runner_args#{idx}_#{test[:test]}" + testname = "#{test[:test]}(#{args})".dump + output.puts(" run_test(#{wrapper}, #{testname}, #{test[:line_number]});") + end + end + end + output.puts + output.puts(' CMock_Guts_MemFreeFinal();') unless used_mocks.empty? + if @options[:has_suite_teardown] + if @options[:omit_begin_end] + output.puts(' (void) suite_teardown(0);') + else + output.puts(' return suiteTearDown(UnityEnd());') + end + else + output.puts(' return UnityEnd();') unless @options[:omit_begin_end] + end + output.puts('}') + end + + def create_h_file(output, filename, tests, testfile_includes, used_mocks) + filename = File.basename(filename).gsub(/[-\/\\\.\,\s]/, '_').upcase + output.puts('/* AUTOGENERATED FILE. DO NOT EDIT. */') + output.puts("#ifndef _#{filename}") + output.puts("#define _#{filename}\n\n") + output.puts("#include \"#{@options[:framework]}.h\"") + output.puts('#include "cmock.h"') unless used_mocks.empty? + @options[:includes].flatten.uniq.compact.each do |inc| + output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}") + end + testfile_includes.each do |inc| + output.puts("#include #{inc.include?('<') ? inc : "\"#{inc}\""}") + end + output.puts "\n" + tests.each do |test| + if test[:params].nil? || test[:params].empty? + output.puts("void #{test[:test]}(void);") + else + output.puts("void #{test[:test]}(#{test[:params]});") + end + end + output.puts("#endif\n\n") + end +end + +if $0 == __FILE__ + options = { includes: [] } + + # parse out all the options first (these will all be removed as we go) + ARGV.reject! do |arg| + case arg + when '-cexception' + options[:plugins] = [:cexception] + true + when /\.*\.ya?ml$/ + options = UnityTestRunnerGenerator.grab_config(arg) + true + when /--(\w+)=\"?(.*)\"?/ + options[Regexp.last_match(1).to_sym] = Regexp.last_match(2) + true + when /\.*\.(?:hpp|hh|H|h)$/ + options[:includes] << arg + true + else false + end + end + + # make sure there is at least one parameter left (the input file) + unless ARGV[0] + puts ["\nusage: ruby #{__FILE__} (files) (options) input_test_file (output)", + "\n input_test_file - this is the C file you want to create a runner for", + ' output - this is the name of the runner file to generate', + ' defaults to (input_test_file)_Runner', + ' files:', + ' *.yml / *.yaml - loads configuration from here in :unity or :cmock', + ' *.h - header files are added as #includes in runner', + ' options:', + ' -cexception - include cexception support', + ' -externc - add extern "C" for cpp support', + ' --setup_name="" - redefine setUp func name to something else', + ' --teardown_name="" - redefine tearDown func name to something else', + ' --main_name="" - redefine main func name to something else', + ' --test_prefix="" - redefine test prefix from default test|spec|should', + ' --test_reset_name="" - redefine resetTest func name to something else', + ' --test_verify_name="" - redefine verifyTest func name to something else', + ' --suite_setup="" - code to execute for setup of entire suite', + ' --suite_teardown="" - code to execute for teardown of entire suite', + ' --use_param_tests=1 - enable parameterized tests (disabled by default)', + ' --omit_begin_end=1 - omit calls to UnityBegin and UnityEnd (disabled by default)', + ' --header_file="" - path/name of test header file to generate too'].join("\n") + exit 1 + end + + # create the default test runner name if not specified + ARGV[1] = ARGV[0].gsub('.c', '_Runner.c') unless ARGV[1] + + UnityTestRunnerGenerator.new(options).run(ARGV[0], ARGV[1]) +end diff --git a/test/tools/meson.build b/test/tools/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..7e9b12294e366202a2a9f41d06af8173b156b206 --- /dev/null +++ b/test/tools/meson.build @@ -0,0 +1,6 @@ +generate_test_runner = find_program('generate_test_runner.rb') +test_runner_generator = generator( + generate_test_runner, + output: ['@BASENAME@_Runner.c'], + arguments: ['@INPUT@', '@OUTPUT@'] +) diff --git a/test/tools/run_test.erb b/test/tools/run_test.erb new file mode 100644 index 0000000000000000000000000000000000000000..f91b566915b10cff86d3d4da119ffe1c5863a6f4 --- /dev/null +++ b/test/tools/run_test.erb @@ -0,0 +1,37 @@ +/*=======Test Runner Used To Run Each Test=====*/ +static void run_test(UnityTestFunction func, const char* name, UNITY_LINE_TYPE line_num) +{ + Unity.CurrentTestName = name; + Unity.CurrentTestLineNumber = line_num; +#ifdef UNITY_USE_COMMAND_LINE_ARGS + if (!UnityTestMatches()) + return; +#endif + Unity.NumberOfTests++; + UNITY_CLR_DETAILS(); + UNITY_EXEC_TIME_START(); + CMock_Init(); + if (TEST_PROTECT()) + { +<% if @options[:plugins].include?(:cexception) %> + CEXCEPTION_T e; + Try { + <%= @options[:setup_name] %>(); + func(); + } Catch(e) { + TEST_ASSERT_EQUAL_HEX32_MESSAGE(CEXCEPTION_NONE, e, "Unhandled Exception!"); + } +<% else %> + <%= @options[:setup_name] %>(); + func(); +<% end %> + } + if (TEST_PROTECT()) + { + <%= @options[:teardown_name] %>(); + CMock_Verify(); + } + CMock_Destroy(); + UNITY_EXEC_TIME_STOP(); + UnityConcludeTest(); +} diff --git a/test/tools/type_sanitizer.rb b/test/tools/type_sanitizer.rb new file mode 100644 index 0000000000000000000000000000000000000000..dafb8826e71ceb74afa2c4a1563ce66e659b230a --- /dev/null +++ b/test/tools/type_sanitizer.rb @@ -0,0 +1,6 @@ +module TypeSanitizer + def self.sanitize_c_identifier(unsanitized) + # convert filename to valid C identifier by replacing invalid chars with '_' + unsanitized.gsub(/[-\/\\\.\,\s]/, '_') + end +end