Skip to content

Initial version of integration tests #330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d50daa3
Initial commit for integration tests. Experimental. Playing with pote…
iboB May 19, 2021
c74a739
Some experimental code to setup tests
iboB May 19, 2021
2830b70
Piecewise building of CMakeLists
iboB May 20, 2021
dbb018e
First check
iboB Jan 6, 2022
57386e7
Alternative approach. Using ruby's test/unit
iboB Jan 10, 2022
77eb0e9
Parse CMakeCache. Separate lib
iboB Jan 10, 2022
10c24fc
First integration test
iboB Jan 11, 2022
b30bdc6
Merge remote-tracking branch 'origin/master' into integration-tests
iboB Jan 12, 2022
473e7bf
Latest Format.cmake. Passing style
iboB Jan 12, 2022
4c9ee1f
Allow user-provided integration test dir. Allow reuse
iboB Jan 12, 2022
e3ffa2c
Separate class with utils for cache (no longer pure Hash)
iboB Jan 12, 2022
caeaf29
Allow running of tests from any dir
iboB Jan 12, 2022
6ef5dfd
Add integration tests to CI
iboB Jan 12, 2022
6f14e4d
Use an in-source integration test directory
iboB Jan 12, 2022
7c9c42f
Allow relative integration test dir from env
iboB Jan 12, 2022
5b162be
Custom assertion for a success of CommandResult
iboB Jan 12, 2022
80cc580
Windows-latest-latest
iboB Jan 12, 2022
058eabe
Enrich CMakeCache class with more CPM data
iboB Jan 12, 2022
aec0f0d
Added test for CPM-specific CMakeCache values
iboB Jan 12, 2022
0ddac3b
Style
iboB Jan 12, 2022
d23788c
Style
iboB Jan 12, 2022
f7ccbcd
test_update_single_package
iboB Jan 12, 2022
cef108e
WiP for source cache test
iboB Jan 12, 2022
8ba308c
Small source_cache test
iboB Jan 13, 2022
74494e6
Style
iboB Jan 13, 2022
3bac936
Moved env clean to cleanup to make setup methods simpler (not require…
iboB Jan 13, 2022
09def1e
WiP for integration test documentation
iboB Jan 13, 2022
9eab191
WiP for integration test documentation
iboB Jan 13, 2022
c7fd76a
Project file creation tweaks
iboB Jan 13, 2022
659e1a3
Split docs into multiple files. Complete tutorial. Reference.
iboB Jan 13, 2022
6b91824
Tips
iboB Jan 13, 2022
358ab4d
Merge remote-tracking branch 'origin/master' into integration-tests
iboB Jan 13, 2022
754d6ed
Typo
iboB Jan 14, 2022
8aa2732
Setup Ruby inistead of requiring windows-2022
iboB Jan 14, 2022
56f4d0c
Revert "Setup Ruby inistead of requiring windows-2022"
iboB Jan 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment on lines +16 to +19
Copy link
Member

@TheLartians TheLartians Jan 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could also use the official ruby/setup-ruby action instead, which is a little more clear in its intent, and requires no explanation.

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7

But I'm also fine with keeping windows-2022.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I didn't realize setting up Ruby would be that easy. I still have the travis/appveyor mindset where installing different versions of software requires complex arcane platform-specific scripts, but of course on github there should be a one-liner for that.

I'm now in favor of keeping windows-latest, as it's always suspicious (at least to me) when a single concrete version of an OS is added in the CI :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well... I was too quick to praise the setup-ruby action. The problem is that it adds mingw in the path and this confuses CMake. We could still work around this, but not over-complicating the CI scripts is a better idea IMO.

So I'm reverting my commit and keeping windows-2022

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Darn, I also would have expected it to just work, but then I agree that windows-2022 is the way to go!


steps:
- name: clone
Expand All @@ -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
2 changes: 2 additions & 0 deletions test/integration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Use this to have a local integration test which is for personal experiments
test_local.rb
44 changes: 44 additions & 0 deletions test/integration/README.md
Original file line number Diff line number Diff line change
@@ -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 `<repo-root>/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_`. `<repo-root>/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)
98 changes: 98 additions & 0 deletions test/integration/idiosyncrasies.md
Original file line number Diff line number Diff line change
@@ -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
```
200 changes: 200 additions & 0 deletions test/integration/lib.rb
Original file line number Diff line number Diff line change
@@ -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
Loading