diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7b90cdc..321b788a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + # windows-latest is windows-2019 which carries a pretty old version of ruby (2.5) + # we need at least ruby 2.7 for the tests + # instead of dealing with installing a modern version of ruby on 2019, we'll just use windows-2022 here + os: [ubuntu-latest, windows-2022, macos-latest] steps: - name: clone @@ -23,3 +26,8 @@ jobs: run: | cmake -Htest -Bbuild/test cmake --build build/test --target test-verbose + + - name: integration tests + run: ruby test/integration/runner.rb + env: + CPM_INTEGRATION_TEST_DIR: ./build/integration diff --git a/test/integration/.gitignore b/test/integration/.gitignore new file mode 100644 index 00000000..43488929 --- /dev/null +++ b/test/integration/.gitignore @@ -0,0 +1,2 @@ +# Use this to have a local integration test which is for personal experiments +test_local.rb diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 00000000..59a68a6d --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,44 @@ +# CPM.cmake Integration Tests + +The integration tests of CPM.cmake are written in Ruby. They use a custom integration test framework which extends the [Test::Unit](https://www.rubydoc.info/github/test-unit/test-unit/Test/Unit) library. + +They require Ruby 2.7.0 or later. + +## Running tests + +To run all tests from the repo root execute: + +``` +$ ruby test/integration/runner.rb +``` + +The runner will run all tests and generate a report of the exeuction. + +The current working directory doesn't matter. If you are in `/test/integration`, you can run simply `$ ruby runner.rb`. + +You can execute with `--help` (`$ ruby runner.rb --help`) to see various configuration options of the runner like running individual tests or test cases, or ones that match a regex. + +The tests themselves are situated in the Ruby scripts prefixed with `test_`. `/test/integration/test_*`. You can also run an individual test script. For example to only run the **basics** test case, you can execute `$ ruby test_basics.rb` + +The tests generate CMake scripts and execute CMake and build toolchains. By default they do this in a directory they generate in your temp path (`/tmp/cpm-test/` on Linux). You can configure the working directory of the tests with an environment variable `CPM_INTEGRATION_TEST_DIR`. For example `$ CPM_INTEGRATION_TEST_DIR=~/mycpmtest; ruby runner.rb` + +## Writing tests + +Writing tests makes use of the custom integration test framework in `lib.rb`. It is a relatively small extension of Ruby's Test::Unit library. + +### The Gist + +* Tests cases are Ruby scripts in this directory. The file names must be prefixed with `test_` +* The script should `require_relative './lib'` to allow for individual execution (or else if will only be executable from the runner) +* A test case file should contain a single class which inherits from `IntegrationTest`. It *can* contain multiple classes, but that's bad practice as it makes individual execution harder and implies a dependency between the classes. +* There should be no dependency between the test scripts. Each should be executable individually and the order in which multiple ones are executed mustn't matter. +* The class should contain methods, also prefixed with `test_` which will be executed by the framework. In most cases there would be a single test method per class. +* In case there are multiple test methods, they will be executed in the order in which they are defined. +* The test methods should contain assertions which check for the expected state of things at varous points of the test's execution. + +### More + +* [A basic tutorial on writing integration tests.](tutorial.md) +* [A brief reference of the integration test framework](reference.md) +* Make sure you're familiar with the [idiosyncrasies](idiosyncrasies.md) of writing integration tests +* [Some tips and tricks](tips.md) diff --git a/test/integration/idiosyncrasies.md b/test/integration/idiosyncrasies.md new file mode 100644 index 00000000..bd4a0e96 --- /dev/null +++ b/test/integration/idiosyncrasies.md @@ -0,0 +1,98 @@ +# Notable Idiosyncrasies When Writing Integration Tests + +As an integration test framework based on a unit test framework the one created for CPM.cmake suffers from several idiosyncrasies. Make sure you familiarize yourself with them before writing integration tests. + +## No shared instance variables between methods + +The runner will create an instance of the test class for each test method. This means that instance variables defined in a test method, *will not* be visible in another. For example: + +```ruby +class MyTest < IntegrationTest + def test_something + @x = 123 + assert_equal 123, @x # Pass. @x is 123 + end + def test_something_else + assert_equal 123, @x # Fail! @x would be nil here + end +end +``` + +There are hacks around sharing Ruby state between methods, but we choose not to use them. If you want to initialize something for all test methods, use `setup`. + +```ruby +class MyTest < IntegrationTest + def setup + @x = 123 + end + def test_something + assert_equal 123, @x # Pass. @x is 123 thanks to setup + end + def test_something_else + assert_equal 123, @x # Pass. @x is 123 thanks to setup + end +end +``` + +## `IntegrationTest` makes use of `Test::Unit::TestCase#cleanup` + +After each test method the `cleanup` method is called thanks to Test::Unit. If you require the use of `cleanup` in your own tests, make sure you call `super` to also run `IntegrationTest#cleanup`. + +```ruby +class MyTest < IntegrationTest + def cleanup + super + my_cleanup + end + # ... +end +``` + +## It's better to have assertions in test methods as opposed to helper methods + +Test::Unit will display a helpful message if an assertion has failed. It will also include the line of code in the test method which caused the failure. However if an assertion is not in the test method, it will display the line which calls the method in which it is. So, please try, to have most assertions in test methods (though we acknowledge that in certain cases this is not practical). For example, if you only require scopes, try using lambdas. + +Instead of this: + +```ruby +class MyTest < IntegrationTest + def test_something + do_a + do_b + do_c + end + def do_a + # ... + end + def do_b + # ... + assert false # will display failed line as "do_b" + end + def do_c + # ... + end +end +``` + +...write this: + +```ruby +class MyTest < IntegrationTest + def test_something + do_a = -> { + # ... + } + do_b = -> { + # ... + assert false # will display failed line as "assert false" + } + do_c = -> { + # ... + } + + do_a.() + do_b.() + do_c.() + end +end +``` diff --git a/test/integration/lib.rb b/test/integration/lib.rb new file mode 100644 index 00000000..6510cf2d --- /dev/null +++ b/test/integration/lib.rb @@ -0,0 +1,200 @@ +require 'fileutils' +require 'open3' +require 'tmpdir' +require 'test/unit' + +module TestLib + TMP_DIR = File.expand_path(ENV['CPM_INTEGRATION_TEST_DIR'] || File.join(Dir.tmpdir, 'cpm-test', Time.now.strftime('%Y_%m_%d-%H_%M_%S'))) + CPM_PATH = File.expand_path('../../cmake/CPM.cmake', __dir__) + + TEMPLATES_DIR = File.expand_path('templates', __dir__) + + # Environment variables which are read by cpm + CPM_ENV = %w( + CPM_USE_LOCAL_PACKAGES + CPM_LOCAL_PACKAGES_ONLY + CPM_DOWNLOAD_ALL + CPM_DONT_UPDATE_MODULE_PATH + CPM_DONT_CREATE_PACKAGE_LOCK + CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + CPM_USE_NAMED_CACHE_DIRECTORIES + CPM_SOURCE_CACHE + ) + def self.clear_env + CPM_ENV.each { ENV[_1] = nil } + end +end + +puts "Warning: test directory '#{TestLib::TMP_DIR}' already exists" if File.exist?(TestLib::TMP_DIR) +raise "Cannot find 'CPM.cmake' at '#{TestLib::CPM_PATH}'" if !File.file?(TestLib::CPM_PATH) + +puts "Running CPM.cmake integration tests" +puts "Temp directory: '#{TestLib::TMP_DIR}'" + +# Clean all CPM-related env vars +TestLib.clear_env + +class Project + def initialize(src_dir, bin_dir) + @src_dir = src_dir + @bin_dir = bin_dir + end + + attr :src_dir, :bin_dir + + def create_file(target_path, text, args = {}) + target_path = File.join(@src_dir, target_path) + + # tweak args + args[:cpm_path] = TestLib::CPM_PATH if !args[:cpm_path] + args[:packages] = [args[:package]] if args[:package] # if args contain package, create the array + args[:packages] = args[:packages].join("\n") if args[:packages] # join all packages if any + + File.write target_path, text % args + end + + def create_file_from_template(target_path, source_path, args = {}) + source_path = File.join(@src_dir, source_path) + raise "#{source_path} doesn't exist" if !File.file?(source_path) + src_text = File.read source_path + create_file target_path, src_text, args + end + + # common function to create ./CMakeLists.txt from ./lists.in.cmake + def create_lists_from_default_template(args = {}) + create_file_from_template 'CMakeLists.txt', 'lists.in.cmake', args + end + + CommandResult = Struct.new :out, :err, :status + def configure(extra_args = '') + CommandResult.new *Open3.capture3("cmake -S #{@src_dir} -B #{@bin_dir} #{extra_args}") + end + def build(extra_args = '') + CommandResult.new *Open3.capture3("cmake --build #{@bin_dir} #{extra_args}") + end + + class CMakeCache + class Entry + def initialize(val, type, advanced, desc) + @val = val + @type = type + @advanced = advanced + @desc = desc + end + attr :val, :type, :advanced, :desc + alias_method :advanced?, :advanced + def inspect + "(#{val.inspect} #{type}" + (advanced? ? ' ADVANCED)' : ')') + end + end + + Package = Struct.new(:ver, :src_dir, :bin_dir) + + def self.from_dir(dir) + entries = {} + cur_desc = '' + file = File.join(dir, 'CMakeCache.txt') + return nil if !File.file?(file) + File.readlines(file).each { |line| + line.strip! + next if line.empty? + next if line.start_with? '#' # comment + if line.start_with? '//' + cur_desc += line[2..] + else + m = /(.+?)(-ADVANCED)?:([A-Z]+)=(.*)/.match(line) + raise "Error parsing '#{line}' in #{file}" if !m + entries[m[1]] = Entry.new(m[4], m[3], !!m[2], cur_desc) + cur_desc = '' + end + } + CMakeCache.new entries + end + + def initialize(entries) + @entries = entries + + package_list = self['CPM_PACKAGES'] + @packages = if package_list + # collect package data + @packages = package_list.split(';').map { |name| + [name, Package.new( + self["CPM_PACKAGE_#{name}_VERSION"], + self["CPM_PACKAGE_#{name}_SOURCE_DIR"], + self["CPM_PACKAGE_#{name}_BINARY_DIR"] + )] + }.to_h + else + {} + end + end + + attr :entries, :packages + + def [](key) + e = @entries[key] + return nil if !e + e.val + end + end + def read_cache + CMakeCache.from_dir @bin_dir + end +end + +class IntegrationTest < Test::Unit::TestCase + self.test_order = :defined # run tests in order of defintion (as opposed to alphabetical) + + def cleanup + # Clear cpm-related env vars which may have been set by the test + TestLib.clear_env + end + + # extra assertions + + def assert_success(res) + msg = build_message(nil, "command status was expected to be a success, but failed with code and STDERR:\n\n#{res.err}", res.status.to_i) + assert_block(msg) { res.status.success? } + end + + def assert_same_path(a, b) + msg = build_message(nil, " expected but was\n", a, b) + assert_block(msg) { File.identical? a, b } + end + + # utils + class << self + def startup + @@test_dir = File.join(TestLib::TMP_DIR, self.name. + # to-underscore conversion from Rails + gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + ) + end + end + + def cur_test_dir + @@test_dir + end + + def make_project(template_dir = nil) + test_name = local_name + test_name = test_name[5..] if test_name.start_with?('test_') + + base = File.join(cur_test_dir, test_name) + src_dir = base + '-src' + + FileUtils.mkdir_p src_dir + + if template_dir + template_dir = File.join(TestLib::TEMPLATES_DIR, template_dir) + raise "#{template_dir} is not a directory" if !File.directory?(template_dir) + FileUtils.copy_entry template_dir, src_dir + end + + Project.new src_dir, base + '-bin' + end +end diff --git a/test/integration/reference.md b/test/integration/reference.md new file mode 100644 index 00000000..fd81450e --- /dev/null +++ b/test/integration/reference.md @@ -0,0 +1,65 @@ +# Integration Test Framework Refernce + +## `TestLib` + +A module for the framework. Provides global data and functionality. For ease of use the utility classes are *not* in this module. + +Provides: + +* `TMP_DIR` - the temporary directory for the current test run +* `CPM_PATH` - path to CPM.cmake. The thing that is being tested +* `TEMPLATES_DIR` - path to integration test templates +* `CPM_ENV` - an array of the names of all environment variables, which CPM.cmake may read +* `.clear_env` - a function to clear all aforementioned environment variables + +## `Project` + +A helper class to manage a CMake project. + +Provides: + +* `#initialize(src_dir, bin_dir)` - create a project with a given source and binary directory +* `#src_dir`, `#bin_dir` - get project directories +* `#create_file(target_path, text, args = {})` - create a file in the project's source directory with a given test. The `args` hash is used to interpolate markup in the text string. + * Will set `:cpm_path` in `args` to `TestLib::CPM_PATH` if not already present. + * If `:package` is present it will be added to the array `:packages` + * Will convert `:packages` form an array to a string +* `#create_file_from_template(target_path, source_path, args = {})` - create a file in the project source directory, based on another file in the project source directory. The contents of the file at `source_path` will be read and used in `create_file` +* `#create_lists_from_default_template(args = {})` - same as `create_file_from_template('CMakeLists.txt', 'lists.in.cmake', args)` +* `::CommandResult` - a struct of: + * `out` - the standard output from a command execution + * `err` - the standard error output from the execution + * `status` - the [`Process::Status`](https://ruby-doc.org/core-2.7.0/Process/Status.html) of the execution +* `#configure(extra_args = '') => CommandResult` - configure the project with optional extra args to CMake +* `#build(extra_args = '') => CommandResult` - build the project with optional extra args to CMake +* `::CMakeCache` - a helper class with the contents of a CMakeCache.txt. Provides: + * `::Entry` - a CMake cache entry of: + * `val` - the value as string + * `type` - the type as string + * `advanced?` - whether the entry is an advanced option + * `desc` - the description of the entry (can be an empty string) + * `::Package` - the CMake cache for a CPM.cmake package. A struct of: + * `ver` - the version as string + * `src_dir`, `bin_dir` - the source and binary directories of the package + * `.from_dir(dir)` - create an instance of `CMakeCache` from `/CMakeLists.txt` + * `#initialize(entries)` - create a cache from a hash of entries by name. Will populate packages. + * `#entries => {String => Entry}` - the entries of the cache + * `#packages => {String => Package}` - CPM.cmake packages by name found in the cache + * `#[](key) => String` - an entry value from an entry name. Created because the value is expected to be needed much more frequently than the entire entry data. To get a full entry use `cache.entries['name']`. +* `read_cache => CMakeCache` - reads the CMake cache in the binary directory of the project and returns it as a `CMakeCache` instance + +## `IntegrationTest` + +The class which must be a parent of all integration test case classes. It itself extends `Test::Unit::TestCase` with: + +### Assertions + +* `assert_success(res)` - assert that an instance of `Project::CommandResult` is a success +* `assert_same_path(a, b)` - assert that two strings represent the same path. For example on Windows `c:\foo` and `C:\Foo` do. + +### Utils + +* `cur_test_dir` - the directory of the current test case. A subdirectory of `TestLib::TMP_DIR` +* `make_project(template_dir = nil)` - create a project from a test method. Will create a the project's source and binary directories as subdirectories of `cur_test_dir`. + * Optionally work with a template directory, in which case it will copy the contents of the template directory (one from `templates`) in the project's source directory. + diff --git a/test/integration/runner.rb b/test/integration/runner.rb new file mode 100644 index 00000000..ddc40823 --- /dev/null +++ b/test/integration/runner.rb @@ -0,0 +1,4 @@ +require_relative './lib' + +exit Test::Unit::AutoRunner::run(true, __dir__) + diff --git a/test/integration/templates/no-deps/lists.in.cmake b/test/integration/templates/no-deps/lists.in.cmake new file mode 100644 index 00000000..ae4e1369 --- /dev/null +++ b/test/integration/templates/no-deps/lists.in.cmake @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(no-deps) + +include("%{cpm_path}") + +add_executable(no-deps main.c) diff --git a/test/integration/templates/no-deps/main.c b/test/integration/templates/no-deps/main.c new file mode 100644 index 00000000..1880718d --- /dev/null +++ b/test/integration/templates/no-deps/main.c @@ -0,0 +1,6 @@ +#include + +int main() { + puts("Hello"); + return 0; +} diff --git a/test/integration/templates/using-adder/lists.in.cmake b/test/integration/templates/using-adder/lists.in.cmake new file mode 100644 index 00000000..d4eee53e --- /dev/null +++ b/test/integration/templates/using-adder/lists.in.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(using-adder) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +include("%{cpm_path}") + +%{packages} + +add_executable(using-adder using-adder.cpp) + +target_link_libraries(using-adder adder) diff --git a/test/integration/templates/using-adder/using-adder.cpp b/test/integration/templates/using-adder/using-adder.cpp new file mode 100644 index 00000000..e8f836d8 --- /dev/null +++ b/test/integration/templates/using-adder/using-adder.cpp @@ -0,0 +1,8 @@ +#include +#include + +int main() { + int sum = adder::add(5, 3); + std::printf("%d\n", sum); + return 0; +} diff --git a/test/integration/templates/using-fibadder/lists.in.cmake b/test/integration/templates/using-fibadder/lists.in.cmake new file mode 100644 index 00000000..5b4e679b --- /dev/null +++ b/test/integration/templates/using-fibadder/lists.in.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(using-fibadder) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +include("%{cpm_path}") + +%{packages} + +add_executable(using-fibadder using-fibadder.cpp) + +target_link_libraries(using-fibadder fibadder) diff --git a/test/integration/templates/using-fibadder/using-fibadder.cpp b/test/integration/templates/using-fibadder/using-fibadder.cpp new file mode 100644 index 00000000..5f51165f --- /dev/null +++ b/test/integration/templates/using-fibadder/using-fibadder.cpp @@ -0,0 +1,8 @@ +#include +#include + +int main() { + int sum = fibadder::fibadd(6, 7); + std::printf("%d\n", sum); + return 0; +} diff --git a/test/integration/test_basics.rb b/test/integration/test_basics.rb new file mode 100644 index 00000000..8083123c --- /dev/null +++ b/test/integration/test_basics.rb @@ -0,0 +1,56 @@ +require_relative './lib' + +# Tests of cpm caches and vars when no packages are used + +class Basics < IntegrationTest + # Test cpm caches with no cpm-related env vars + def test_cpm_default + prj = make_project 'no-deps' + prj.create_lists_from_default_template + assert_success prj.configure + + @cache = prj.read_cache + + assert_empty @cache.packages + + assert_same_path TestLib::CPM_PATH, check_and_get('CPM_FILE') + assert_same_path File.dirname(TestLib::CPM_PATH), check_and_get('CPM_DIRECTORY') + + assert_equal 'OFF', check_and_get('CPM_DRY_RUN') + assert_equal 'CPM:', check_and_get('CPM_INDENT') + assert_equal '1.0.0-development-version', check_and_get('CPM_VERSION') + + assert_equal 'OFF', check_and_get('CPM_SOURCE_CACHE', 'PATH') + assert_equal 'OFF', check_and_get('CPM_DOWNLOAD_ALL', 'BOOL') + assert_equal 'OFF', check_and_get('CPM_LOCAL_PACKAGES_ONLY', 'BOOL') + assert_equal 'OFF', check_and_get('CPM_USE_LOCAL_PACKAGES', 'BOOL') + assert_equal 'OFF', check_and_get('CPM_USE_NAMED_CACHE_DIRECTORIES', 'BOOL') + + assert_equal 'OFF', check_and_get('CPM_DONT_CREATE_PACKAGE_LOCK', 'BOOL') + assert_equal 'OFF', check_and_get('CPM_INCLUDE_ALL_IN_PACKAGE_LOCK', 'BOOL') + assert_same_path File.join(prj.bin_dir, 'cpm-package-lock.cmake'), check_and_get('CPM_PACKAGE_LOCK_FILE') + + assert_equal 'OFF', check_and_get('CPM_DONT_UPDATE_MODULE_PATH', 'BOOL') + assert_same_path File.join(prj.bin_dir, 'CPM_modules'), check_and_get('CPM_MODULE_PATH') + end + + # Test when env CPM_SOURCE_CACHE is set + def test_env_cpm_source_cache + ENV['CPM_SOURCE_CACHE'] = cur_test_dir + + prj = make_project 'no-deps' + prj.create_lists_from_default_template + assert_success prj.configure + + @cache = prj.read_cache + + assert_equal cur_test_dir, check_and_get('CPM_SOURCE_CACHE', 'PATH') + end + + def check_and_get(key, type = 'INTERNAL') + e = @cache.entries[key] + assert_not_nil e, key + assert_equal type, e.type, key + e.val + end +end diff --git a/test/integration/test_simple.rb b/test/integration/test_simple.rb new file mode 100644 index 00000000..ff175bf2 --- /dev/null +++ b/test/integration/test_simple.rb @@ -0,0 +1,91 @@ +require_relative './lib' + +class Simple < IntegrationTest + ADDER_PACKAGE_NAME = 'testpack-adder' + + def test_update_single_package + prj = make_project 'using-adder' + adder_cache0 = nil + adder_ver_file = nil + + create_with_commit_sha = -> { + prj.create_lists_from_default_template package: + 'CPMAddPackage("gh:cpm-cmake/testpack-adder#cad1cd4b4cdf957c5b59e30bc9a1dd200dbfc716")' + assert_success prj.configure + + cache = prj.read_cache + assert_equal 1, cache.packages.size + + adder_cache = cache.packages[ADDER_PACKAGE_NAME] + assert_not_nil adder_cache + assert_equal '0', adder_cache.ver + assert File.directory? adder_cache.src_dir + assert File.directory? adder_cache.bin_dir + + adder_ver_file = File.join(adder_cache.src_dir, 'version') + assert File.file? adder_ver_file + assert_equal 'initial', File.read(adder_ver_file).strip + + # calculated adder values + assert_equal 'ON', cache['ADDER_BUILD_EXAMPLES'] + assert_equal 'ON', cache['ADDER_BUILD_TESTS'] + assert_equal adder_cache.src_dir, cache['adder_SOURCE_DIR'] + assert_equal adder_cache.bin_dir, cache['adder_BINARY_DIR'] + + # store for future comparisons + adder_cache0 = adder_cache + } + update_to_version_1 = -> { + prj.create_lists_from_default_template package: + 'CPMAddPackage("gh:cpm-cmake/testpack-adder@1.0.0")' + assert_success prj.configure + + cache = prj.read_cache + assert_equal 1, cache.packages.size + + adder_cache = cache.packages[ADDER_PACKAGE_NAME] + assert_not_nil adder_cache + assert_equal '1.0.0', adder_cache.ver + + # dirs shouldn't have changed + assert_equal adder_cache0.src_dir, adder_cache.src_dir + assert_equal adder_cache0.bin_dir, adder_cache.bin_dir + + assert_equal '1.0.0', File.read(adder_ver_file).strip + } + update_with_option_off_and_build = -> { + prj.create_lists_from_default_template package: <<~PACK + CPMAddPackage( + NAME testpack-adder + GITHUB_REPOSITORY cpm-cmake/testpack-adder + VERSION 1.0.0 + OPTIONS "ADDER_BUILD_TESTS OFF" + ) + PACK + assert_success prj.configure + assert_success prj.build + + exe_dir = File.join(prj.bin_dir, 'bin') + assert File.directory? exe_dir + + exes = Dir[exe_dir + '/**/*'].filter { + # on multi-configuration generators (like Visual Studio) the executables will be in bin/ + # also filter-out other articacts like .pdb or .dsym + !File.directory?(_1) && File.stat(_1).executable? + }.map { + # remove .exe extension if any (there will be one on Windows) + File.basename(_1, '.exe') + }.sort + + # we should end up with two executables + # * simple - the simple example from adder + # * using-adder - for this project + # ...and notably no test for adder, which must be disabled from the option override from above + assert_equal ['simple', 'using-adder'], exes + } + + create_with_commit_sha.() + update_to_version_1.() + update_with_option_off_and_build.() + end +end diff --git a/test/integration/test_source_cache.rb b/test/integration/test_source_cache.rb new file mode 100644 index 00000000..fd8f92d3 --- /dev/null +++ b/test/integration/test_source_cache.rb @@ -0,0 +1,80 @@ +require_relative './lib' + +# Tests with source cache + +class SourceCache < IntegrationTest + def setup + @cache_dir = File.join(cur_test_dir, 'cpmcache') + ENV['CPM_SOURCE_CACHE'] = @cache_dir + end + + def test_add_remove_dependency + prj = make_project 'using-fibadder' + + ################################### + # create + prj.create_lists_from_default_template package: 'CPMAddPackage("gh:cpm-cmake/testpack-fibadder@1.0.0")' + assert_success prj.configure + + @cache = prj.read_cache + + # fibadder - adder + # \ fibonacci - Format + assert_equal 4, @cache.packages.size + + check_package_cache 'testpack-fibadder', '1.0.0', '6a17d24c95c44a169ff8ba173f52876a2ba3d137' + check_package_cache 'testpack-adder', '1.0.0', '1a4c153849d8e0cf9a3a245e5f6ab6e4722d8995' + check_package_cache 'testpack-fibonacci', '2.0', '332c789cb09b8c2f92342dfb874c82bec643daf6' + check_package_cache 'Format.cmake', '1.0', 'c5897bd28c5032d45f7f669c8fb470790d2ae156' + + ################################### + # add one package with a newer version + prj.create_lists_from_default_template packages: [ + 'CPMAddPackage("gh:cpm-cmake/testpack-adder@1.0.1")', + 'CPMAddPackage("gh:cpm-cmake/testpack-fibadder@1.0.0")', + ] + assert_success prj.configure + + @cache = prj.read_cache + assert_equal 4, @cache.packages.size + + check_package_cache 'testpack-fibadder', '1.0.0', '6a17d24c95c44a169ff8ba173f52876a2ba3d137' + check_package_cache 'testpack-adder', '1.0.1', '84eb33c1b8db880083cefc2adf4dc3f04778cd44' + check_package_cache 'testpack-fibonacci', '2.0', '332c789cb09b8c2f92342dfb874c82bec643daf6' + check_package_cache 'Format.cmake', '1.0', 'c5897bd28c5032d45f7f669c8fb470790d2ae156' + end + + def test_second_project + prj = make_project 'using-fibadder' + prj.create_lists_from_default_template package: 'CPMAddPackage("gh:cpm-cmake/testpack-fibadder@1.1.0")' + assert_success prj.configure + + @cache = prj.read_cache + + # fibadder - adder + # \ fibonacci - Format + assert_equal 4, @cache.packages.size + + check_package_cache 'testpack-fibadder', '1.1.0', '603d79d88d7230cc749460a0f476df862aa70ead' + check_package_cache 'testpack-adder', '1.0.1', '84eb33c1b8db880083cefc2adf4dc3f04778cd44' + check_package_cache 'testpack-fibonacci', '2.0', '332c789cb09b8c2f92342dfb874c82bec643daf6' + check_package_cache 'Format.cmake', '1.0', 'c5897bd28c5032d45f7f669c8fb470790d2ae156' + end + + def test_cache_dir_contents + num_subdirs = -> (name) { Dir["#{File.join(@cache_dir, name.downcase)}/*/"].size } + assert_equal 2, num_subdirs.('testpack-fibadder') + assert_equal 2, num_subdirs.('testpack-adder') + assert_equal 1, num_subdirs.('testpack-fibonacci') + assert_equal 1, num_subdirs.('Format.cmake') + end + + def check_package_cache(name, ver, dir_sha1) + package = @cache.packages[name] + assert_not_nil package, name + assert_equal ver, package.ver + expected_parent_dir = File.join(@cache_dir, name.downcase) + assert package.src_dir.start_with?(expected_parent_dir), "#{package.src_dir} must be in #{expected_parent_dir}" + assert_equal dir_sha1, File.basename(package.src_dir) + end +end diff --git a/test/integration/tips.md b/test/integration/tips.md new file mode 100644 index 00000000..4ed3eb7d --- /dev/null +++ b/test/integration/tips.md @@ -0,0 +1,35 @@ +# Tips and Tricks + +## Playing and experimenting + +Create a file called `test_local.rb` in this directory to have an integration test which is for your personal experiments and just playing with the integration test framework. `test_local.rb` is gitignored. + +## Speeding-up development + +Running an integration test requires configuring directories with CMake which can be quite slow. To speed-up development of integration tests consider doing the following steps: + +**Work with standalone tests** + +Instead of starting the runner, run just your integration test (`$ ruby test_your_test.rb`). This won't burden the execution with the others. + +**Export the environment variable `CPM_INTEGRATION_TEST_DIR` to some local directory** + +By default the framework generates a new temporary directory for each test run. If you override the temp directory to a specific one, rerunning the tests will work with the binary directories from the previous run and will improve the performance considerably. + +*NOTE HOWEVER* that in certain cases this may not be an option. Some tests might assert that certain artifacts in the temporary directory are missing but upon rerunning in an existing directory they will be there causing the test to fail. + +*ALSO NOTE* that this may silently affect reruns based on CMake caches from previous runs. If your test fails in peculiar ways on reruns, try a clean run. Always do a clean run before declaring a test a success. + +**Set `CPM_SOURCE_CACHE` even if the test doesn't require it** + +This is not a option for tests which explicitly check that there is no source cache. However certain tests may be indiferent to this. For such cases in development, you can add a setup function in the lines of: + +```ruby +def setup + ENV['CPM_SOURCE_CACHE'] = '/home/myself/.testcpmcache' +end +``` + +Then the packages from your test will be cached and not redownloaded every time which is a dramatic improvement in performance. + +*NOTE HOWEVER* that this may introduce subtle bugs. Always test without this dev-only addition, before declaring a test a success. diff --git a/test/integration/tutorial.md b/test/integration/tutorial.md new file mode 100644 index 00000000..17eea960 --- /dev/null +++ b/test/integration/tutorial.md @@ -0,0 +1,69 @@ +# Integration Test Tutorial + +Let's create an integration test which checks that CPM.cmake can make a specific package available. + +First we do some boilerplate. + +```ruby +require_relative './lib' + +class MyTest < IntegrationTest + # test that CPM.cmake can make https://github.com/cpm-cmake/testpack-adder/ available as a package + def test_make_adder_available + end +end +``` + +Now we have our test case class, and the single test method that we will require. Let's focus on the method's contents. The integration test framework provides us with a helper class, `Project`, which can be used for this scenario. A project has an assoiciated pair of source and binary directories in the temporary directory and it provides methods to work with them. + +We start by creating the project: + +```ruby +prj = make_project +``` + +`make_project` is method of IntegrationTest which generates a source and a binary directory for it based on the name of our test class and test method. The project doesn't contain anything yet, so let's create some source files: + +```ruby +prj.create_file 'main.cpp', <<~SRC + #include + #include + int main() { + std::cout << adder::add(1, 2) << '\\n'; + return 0; + } +SRC +prj.create_file 'CMakeLists.txt', <<~SRC + cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + project(using-adder) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + include("%{cpm_path}") + CPMAddPackage("gh:cpm-cmake/testpack-adder@1.0.0") + add_executable(using-adder main.cpp) + target_link_libraries(using-adder adder) +SRC +``` + +Note the line `include("%{cpm_path}")` when creating `CMakeLists.txt`. It contains a markup `%{cpm_path}`. `Project#create_file` will see such markups and substitute them with the appropriate values (in this case the path to CPM.cmake). + +Now that we have the two files we need it's time we configure our project. We can use the opportunity to assert that the configure is successful as we expect it to be. + +```ruby +assert_success prj.configure +``` + +Now we can read the generated `CMakeCache.txt` and assert that certain values we expect are inside. `Project` provides a method for that: `read_cache`. It will return an instance of `Project::CMakeCache` which contains the data from the cache and provides additional helper functionalities. One of them is `packages`, which is a hash of the CPM.cmake packages in the cache with their versions, binary, source directories. So let's get the cache and assert that there is only one CPM.cmake package inside ant it has the version we expect. + +```ruby +cache = prj.read_cache +assert_equal 1, cache.packages.size +assert_equal '1.0.0', cache.packages['testpack-adder'].ver +``` + +Finally let's assert that the project can be built. This would mean that CPM.cmake has made the package available to our test project and that it has the appropriate include directories and link libraries to make an executable out of `main.cpp`. + +```ruby +assert_success prj.build +``` + +You can see the entire code for this tutorial in [tutorial.rb](tutorial.rb) in this directory. diff --git a/test/integration/tutorial.rb b/test/integration/tutorial.rb new file mode 100644 index 00000000..67e44ea5 --- /dev/null +++ b/test/integration/tutorial.rb @@ -0,0 +1,37 @@ +# This file is intentionally not prefixed with test_ +# It is a tutorial for making integration tests and is not to be run from the runner +require_relative './lib' + +class Tutorial < IntegrationTest + # test that CPM.cmake can make https://github.com/cpm-cmake/testpack-adder/ available as a package + def test_make_adder_available + prj = make_project + + prj.create_file 'main.cpp', <<~SRC + #include + #include + int main() { + std::cout << adder::add(1, 2) << '\\n'; + return 0; + } + SRC + + prj.create_file 'CMakeLists.txt', <<~SRC + cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + project(using-adder) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + include("%{cpm_path}") + CPMAddPackage("gh:cpm-cmake/testpack-adder@1.0.0") + add_executable(using-adder main.cpp) + target_link_libraries(using-adder adder) + SRC + + assert_success prj.configure + + cache = prj.read_cache + assert_equal 1, cache.packages.size + assert_equal '1.0.0', cache.packages['testpack-adder'].ver + + assert_success prj.build + end +end diff --git a/test/style/CMakeLists.txt b/test/style/CMakeLists.txt index 2beb38ba..9def3da1 100644 --- a/test/style/CMakeLists.txt +++ b/test/style/CMakeLists.txt @@ -6,7 +6,9 @@ include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake) CPMAddPackage( NAME Format.cmake - VERSION 1.6 + VERSION 1.7.3 GITHUB_REPOSITORY TheLartians/Format.cmake - OPTIONS "FORMAT_CHECK_CMAKE ON" + # We exclude cmake files from integration tests as they contain invalid lines of code which are + # used by the integration test scripts + OPTIONS "CMAKE_FORMAT_EXCLUDE integration/templates" )