-
Notifications
You must be signed in to change notification settings - Fork 200
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
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 c74a739
Some experimental code to setup tests
iboB 2830b70
Piecewise building of CMakeLists
iboB dbb018e
First check
iboB 57386e7
Alternative approach. Using ruby's test/unit
iboB 77eb0e9
Parse CMakeCache. Separate lib
iboB 10c24fc
First integration test
iboB b30bdc6
Merge remote-tracking branch 'origin/master' into integration-tests
iboB 473e7bf
Latest Format.cmake. Passing style
iboB 4c9ee1f
Allow user-provided integration test dir. Allow reuse
iboB e3ffa2c
Separate class with utils for cache (no longer pure Hash)
iboB caeaf29
Allow running of tests from any dir
iboB 6ef5dfd
Add integration tests to CI
iboB 6f14e4d
Use an in-source integration test directory
iboB 7c9c42f
Allow relative integration test dir from env
iboB 5b162be
Custom assertion for a success of CommandResult
iboB 80cc580
Windows-latest-latest
iboB 058eabe
Enrich CMakeCache class with more CPM data
iboB aec0f0d
Added test for CPM-specific CMakeCache values
iboB 0ddac3b
Style
iboB d23788c
Style
iboB f7ccbcd
test_update_single_package
iboB cef108e
WiP for source cache test
iboB 8ba308c
Small source_cache test
iboB 74494e6
Style
iboB 3bac936
Moved env clean to cleanup to make setup methods simpler (not require…
iboB 09def1e
WiP for integration test documentation
iboB 9eab191
WiP for integration test documentation
iboB c7fd76a
Project file creation tweaks
iboB 659e1a3
Split docs into multiple files. Complete tutorial. Reference.
iboB 6b91824
Tips
iboB 358ab4d
Merge remote-tracking branch 'origin/master' into integration-tests
iboB 754d6ed
Typo
iboB 8aa2732
Setup Ruby inistead of requiring windows-2022
iboB 56f4d0c
Revert "Setup Ruby inistead of requiring windows-2022"
iboB File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.But I'm also fine with keeping
windows-2022
.There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!