require 'fileutils' require "open3" require "set" describe Rscons do def rm_rf(dir) FileUtils.rm_rf(dir) if File.exists?(dir) sleep 0.2 FileUtils.rm_rf(dir) if File.exists?(dir) sleep 0.5 FileUtils.rm_rf(dir) if File.exists?(dir) sleep 1.0 FileUtils.rm_rf(dir) if File.exists?(dir) raise "Could not remove #{dir}" end end end end end before(:all) do @statics = {} @build_test_run_dir = "build_test_run" @run_results = Struct.new(:stdout, :stderr, :status) @owd = Dir.pwd rm_rf(@build_test_run_dir) end after(:each) do Dir.chdir(@owd) rm_rf(@build_test_run_dir) end def test_dir(build_test_directory) Dir.chdir(@owd) rm_rf(@build_test_run_dir) FileUtils.cp_r("build_tests/#{build_test_directory}", @build_test_run_dir) Dir.chdir(@build_test_run_dir) end def file_sub(fname) contents = File.read(fname) replaced = '' contents.each_line do |line| replaced += yield(line) end File.open(fname, 'wb') do |fh| fh.write(replaced) end end def run_test(options = {}) rsconsfile_args = if options[:rsconsfile] %W[-f #{options[:rsconsfile]}] else [] end rscons_args = options[:rscons_args] || [] command = %W[ruby -I. -r _simplecov_setup #{@owd}/bin/rscons] + rsconsfile_args + rscons_args @statics[:build_test_id] ||= 0 @statics[:build_test_id] += 1 command_prefix = if ENV["partial_specs"] "p" else "b" end command_name = "#{command_prefix}#{@statics[:build_test_id]}" File.open("_simplecov_setup.rb", "w") do |fh| fh.puts < :bar})).to be_truthy expect(slines.include?(%{CFLAGS => ["-O2", "-fomit-frame-pointer"]})).to be_truthy expect(slines.include?(%{CPPPATH => []})).to be_truthy end it "considers deep dependencies when deciding whether to rerun Preprocess builder" do test_dir("preprocess") result = run_test expect(result.stderr).to eq "" expect(result.stdout).to eq("Preprocess pp\n") expect(File.read("pp")).to match(%r{xyz42abc}m) result = run_test expect(result.stderr).to eq "" expect(result.stdout).to eq "" File.open("bar.h", "w") do |fh| fh.puts "#define BAR abc88xyz" end result = run_test expect(result.stderr).to eq "" expect(result.stdout).to eq("Preprocess pp\n") expect(File.read("pp")).to match(%r{abc88xyz}m) end it "allows construction variable references which expand to arrays in sources of a build target" do test_dir("simple") result = run_test(rsconsfile: "cvar_array.rb") expect(result.stderr).to eq "" expect(File.exists?("build/simple.o")).to be_truthy expect(`./simple.exe`).to eq "This is a simple C program\n" end it "supports registering multiple build targets with the same target path" do test_dir("build_dir") result = run_test(rsconsfile: "multiple_targets_same_name.rb") expect(result.stderr).to eq "" expect(File.exists?("one.o")).to be_truthy expect(lines(result.stdout)).to eq([ "CC one.o", "CC one.o", ]) end it "expands target and source paths when builders are registered in build hooks" do test_dir("build_dir") result = run_test(rsconsfile: "post_build_hook_expansion.rb") expect(result.stderr).to eq "" expect(File.exists?("one.o")).to be_truthy expect(File.exists?("two.o")).to be_truthy expect(lines(result.stdout)).to eq([ "CC one.o", "CC two.o", ]) end it "does not re-run previously successful builders if one fails" do test_dir('simple') File.open("two.c", "w") do |fh| fh.puts("FOO") end result = run_test(rsconsfile: "cache_successful_builds_when_one_fails.rb", rscons_args: %w[-j1]) expect(result.stderr).to match /FOO/ expect(File.exists?("simple.o")).to be_truthy expect(File.exists?("two.o")).to be_falsey File.open("two.c", "w") {|fh|} result = run_test(rsconsfile: "cache_successful_builds_when_one_fails.rb", rscons_args: %w[-j1]) expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC two.o", ] end it "allows overriding PROGSUFFIX" do test_dir("simple") result = run_test(rsconsfile: "progsuffix.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.out", ] end it "does not use PROGSUFFIX when the Program target name expands to a value already containing an extension" do test_dir("simple") result = run_test(rsconsfile: "progsuffix2.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.out", ] end it "allows overriding PROGSUFFIX from extra vars passed in to the builder" do test_dir("simple") result = run_test(rsconsfile: "progsuffix3.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.xyz", ] end it "creates object files under the build root for absolute source paths" do test_dir("simple") result = run_test(rsconsfile: "absolute_source_path.rb") expect(result.stderr).to eq "" slines = lines(result.stdout) expect(slines[0]).to match(%r{^CC build/.*/abs\.o$}) expect(slines[1]).to eq "LD abs.exe" end it "creates shared libraries" do test_dir("shared_library") result = run_test expect(result.stderr).to eq "" slines = lines(result.stdout) if RUBY_PLATFORM =~ /mingw/ expect(slines).to include("SHLD mine.dll") expect(File.exists?("mine.dll")).to be_truthy else expect(slines).to include("SHLD libmine.so") expect(File.exists?("libmine.so")).to be_truthy end result = run_test expect(result.stderr).to eq "" expect(result.stdout).to eq "" ld_library_path_prefix = (RUBY_PLATFORM =~ /mingw/ ? "" : "LD_LIBRARY_PATH=. ") expect(`#{ld_library_path_prefix}./test-shared.exe`).to match /Hi from one/ expect(`./test-static.exe`).to match /Hi from one/ end it "creates shared libraries using C++" do test_dir("shared_library") result = run_test(rsconsfile: "shared_library_cxx.rb") expect(result.stderr).to eq "" slines = lines(result.stdout) if RUBY_PLATFORM =~ /mingw/ expect(slines).to include("SHLD mine.dll") else expect(slines).to include("SHLD libmine.so") end result = run_test(rsconsfile: "shared_library_cxx.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" ld_library_path_prefix = (RUBY_PLATFORM =~ /mingw/ ? "" : "LD_LIBRARY_PATH=. ") expect(`#{ld_library_path_prefix}./test-shared.exe`).to match /Hi from one/ expect(`./test-static.exe`).to match /Hi from one/ end it "raises an error for a circular dependency" do test_dir("simple") result = run_test(rsconsfile: "error_circular_dependency.rb") expect(result.stderr).to match /Possible circular dependency for (foo|bar|baz)/ expect(result.status).to_not eq 0 end it "raises an error for a circular dependency where a build target contains itself in its source list" do test_dir("simple") result = run_test(rsconsfile: "error_circular_dependency2.rb") expect(result.stderr).to match /Possible circular dependency for foo/ expect(result.status).to_not eq 0 end context "backward compatibility" do it "allows a builder to call Environment#run_builder in a non-threaded manner" do test_dir("simple") result = run_test(rsconsfile: "run_builder.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC simple.o", "LD simple.exe", ] end it "allows a builder to call Environment#build_sources in a non-threaded manner" do test_dir("simple") result = run_test(rsconsfile: "build_sources.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC simple.o", "CC build/two.o", "MyProgram simple.exe", ] end it "prints the failed build command for a threaded builder when called via Environment#run_builder without delayed execution" do test_dir("simple") File.open("simple.c", "wb") do |fh| fh.write("FOOBAR") end result = run_test(rsconsfile: "run_builder.rb") expect(result.stderr).to match /Failed to build/ expect(result.stdout).to match /Failed command was: gcc/ end it "supports builders that call Builder#standard_build" do test_dir("simple") result = run_test(rsconsfile: "standard_build.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["MyCommand simple.o"] end it "supports the old 3-parameter signature to Builder#produces?" do test_dir("simple") result = run_test(rsconsfile: "bc_produces.rb") expect(result.stderr).to eq "" end it 'supports build hooks to override construction variables' do test_dir("build_dir") result = run_test(rsconsfile: "backward_compatible_build_hooks.rb") expect(result.stderr).to eq "" expect(Set[*lines(result.stdout)]).to eq Set[ 'gcc -c -o one.o -MMD -MF one.mf -Isrc/one/ -Isrc/two/ -O1 src/two/two.c', 'gcc -c -o two.o -MMD -MF two.mf -Isrc/one/ -Isrc/two/ -O2 src/two/two.c' ] expect(File.exists?('one.o')).to be_truthy expect(File.exists?('two.o')).to be_truthy end end context "CFile builder" do it "builds a .c file using flex and bison" do test_dir("cfile") result = run_test expect(result.stderr).to eq "" expect(Set[*lines(result.stdout)]).to eq Set[ "LEX lexer.c", "YACC parser.c", ] result = run_test expect(result.stderr).to eq "" expect(result.stdout).to eq "" end it "raises an error when an unknown source file is specified" do test_dir("cfile") result = run_test(rsconsfile: "error_unknown_extension.rb") expect(result.stderr).to match /Unknown source file .foo.bar. for CFile builder/ expect(result.status).to_not eq 0 end end context "Command builder" do it "allows executing an arbitrary command" do test_dir('simple') result = run_test(rsconsfile: "command_builder.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["BuildIt simple.exe"] expect(`./simple.exe`).to eq "This is a simple C program\n" result = run_test(rsconsfile: "command_builder.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" end it "allows redirecting standard output to a file" do test_dir("simple") result = run_test(rsconsfile: "command_redirect.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC simple.o", "My Disassemble simple.txt", ] expect(File.read("simple.txt")).to match /Disassembly of section .text:/ end end context "Directory builder" do it "creates the requested directory" do test_dir("simple") result = run_test(rsconsfile: "directory.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq(["Directory teh_dir"]) expect(File.directory?("teh_dir")).to be_truthy end it "succeeds when the requested directory already exists" do test_dir("simple") FileUtils.mkdir("teh_dir") result = run_test(rsconsfile: "directory.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" expect(File.directory?("teh_dir")).to be_truthy end it "fails when the target path is a file" do test_dir("simple") FileUtils.touch("teh_dir") result = run_test(rsconsfile: "directory.rb") expect(result.stderr).to match %r{Error: `teh_dir' already exists and is not a directory} end end context "Install buildler" do it "copies a file to the target file name" do test_dir("build_dir") result = run_test(rsconsfile: "install.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Install inst.exe"] result = run_test(rsconsfile: "install.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" expect(File.exists?("inst.exe")).to be_truthy expect(File.read("inst.exe", mode: "rb")).to eq(File.read("install.rb", mode: "rb")) FileUtils.rm("inst.exe") result = run_test(rsconsfile: "install.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Install inst.exe"] end it "operates the same as a Copy builder" do test_dir("build_dir") result = run_test(rsconsfile: "copy.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Copy inst.exe"] result = run_test(rsconsfile: "copy.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" expect(File.exists?("inst.exe")).to be_truthy expect(File.read("inst.exe", mode: "rb")).to eq(File.read("copy.rb", mode: "rb")) FileUtils.rm("inst.exe") result = run_test(rsconsfile: "copy.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Copy inst.exe"] end it "copies a file to the target directory name" do test_dir "build_dir" result = run_test(rsconsfile: "install_directory.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include("Install inst") expect(File.exists?("inst/install_directory.rb")).to be_truthy expect(File.read("inst/install_directory.rb", mode: "rb")).to eq(File.read("install_directory.rb", mode: "rb")) result = run_test(rsconsfile: "install_directory.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" end it "copies a directory to the non-existent target directory name" do test_dir "build_dir" result = run_test(rsconsfile: "install_directory.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include("Install noexist/src") %w[src/one/one.c src/two/two.c src/two/two.h].each do |f| expect(File.exists?("noexist/#{f}")).to be_truthy expect(File.read("noexist/#{f}", mode: "rb")).to eq(File.read(f, mode: "rb")) end end it "copies a directory to the existent target directory name" do test_dir "build_dir" result = run_test(rsconsfile: "install_directory.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include("Install exist/src") %w[src/one/one.c src/two/two.c src/two/two.h].each do |f| expect(File.exists?("exist/#{f}")).to be_truthy expect(File.read("exist/#{f}", mode: "rb")).to eq(File.read(f, mode: "rb")) end end end context "phony targets" do it "allows specifying a Symbol as a target name and reruns the builder if the sources or command have changed" do test_dir("simple") result = run_test(rsconsfile: "phony_target.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq([ "CC build/simple.o", "LD simple.exe", "Checker simple.exe", ]) result = run_test(rsconsfile: "phony_target.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" FileUtils.cp("phony_target.rb", "phony_target2.rb") file_sub("phony_target2.rb") {|line| line.sub(/.*Program.*/, "")} File.open("simple.exe", "w") do |fh| fh.puts "Changed simple.exe" end result = run_test(rsconsfile: "phony_target2.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq([ "Checker simple.exe", ]) end end context "Environment#clear_targets" do it "clears registered targets" do test_dir("simple") result = run_test(rsconsfile: "clear_targets.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" end end context "Cache management" do it "prints a warning when the cache is corrupt" do test_dir("simple") File.open(Rscons::Cache::CACHE_FILE, "w") do |fh| fh.puts("[1]") end result = run_test expect(result.stderr).to match /Warning.*was corrupt. Contents:/ end it "removes the cache file on a clean operation" do test_dir("simple") result = run_test expect(result.stderr).to eq "" expect(File.exists?(Rscons::Cache::CACHE_FILE)).to be_truthy result = run_test(rscons_args: %w[-c]) expect(result.stderr).to eq "" expect(File.exists?(Rscons::Cache::CACHE_FILE)).to be_falsey end it "forces a build when the target file does not exist and is not in the cache" do test_dir("simple") expect(File.exists?("simple.exe")).to be_falsey result = run_test expect(result.stderr).to eq "" expect(File.exists?("simple.exe")).to be_truthy end it "forces a build when the target file does exist but is not in the cache" do test_dir("simple") File.open("simple.exe", "wb") do |fh| fh.write("hi") end result = run_test expect(result.stderr).to eq "" expect(File.exists?("simple.exe")).to be_truthy expect(File.read("simple.exe", mode: "rb")).to_not eq "hi" end it "forces a build when the target file exists and is in the cache but has changed since cached" do test_dir("simple") result = run_test expect(result.stderr).to eq "" File.open("simple.exe", "wb") do |fh| fh.write("hi") end test_dir("simple") result = run_test expect(result.stderr).to eq "" expect(File.exists?("simple.exe")).to be_truthy expect(File.read("simple.exe", mode: "rb")).to_not eq "hi" end it "forces a build when the command changes" do test_dir("simple") result = run_test expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.exe", ] result = run_test(rsconsfile: "cache_command_change.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "LD simple.exe", ] end it "forces a build when there is a new dependency" do test_dir("simple") result = run_test(rsconsfile: "cache_new_dep1.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC simple.o", "LD simple.exe", ] result = run_test(rsconsfile: "cache_new_dep2.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "LD simple.exe", ] end it "forces a build when a dependency's checksum has changed" do test_dir("simple") result = run_test(rsconsfile: "cache_dep_checksum_change.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Copy simple.copy"] File.open("simple.c", "wb") do |fh| fh.write("hi") end result = run_test(rsconsfile: "cache_dep_checksum_change.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq ["Copy simple.copy"] end it "forces a rebuild with strict_deps=true when dependency order changes" do test_dir("two_sources") File.open("sources", "wb") do |fh| fh.write("one.o two.o") end result = run_test(rsconsfile: "cache_strict_deps.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include "gcc -o program.exe one.o two.o" result = run_test(rsconsfile: "cache_strict_deps.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" File.open("sources", "wb") do |fh| fh.write("two.o one.o") end result = run_test(rsconsfile: "cache_strict_deps.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include "gcc -o program.exe one.o two.o" end it "forces a rebuild when there is a new user dependency" do test_dir("simple") File.open("foo", "wb") {|fh| fh.write("hi")} File.open("user_deps", "wb") {|fh| fh.write("")} result = run_test(rsconsfile: "cache_user_dep.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.exe", ] File.open("user_deps", "wb") {|fh| fh.write("foo")} result = run_test(rsconsfile: "cache_user_dep.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "LD simple.exe", ] end it "forces a rebuild when a user dependency file checksum has changed" do test_dir("simple") File.open("foo", "wb") {|fh| fh.write("hi")} File.open("user_deps", "wb") {|fh| fh.write("foo")} result = run_test(rsconsfile: "cache_user_dep.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "CC build/simple.o", "LD simple.exe", ] result = run_test(rsconsfile: "cache_user_dep.rb") expect(result.stderr).to eq "" expect(result.stdout).to eq "" File.open("foo", "wb") {|fh| fh.write("hi2")} result = run_test(rsconsfile: "cache_user_dep.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "LD simple.exe", ] end end context "Object builder" do it "allows overriding CCCMD construction variable" do test_dir("simple") result = run_test(rsconsfile: "override_cccmd.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "gcc -c -o simple.o -Dfoobar simple.c", ] end it "allows overriding DEPFILESUFFIX construction variable" do test_dir("simple") result = run_test(rsconsfile: "override_depfilesuffix.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to eq [ "gcc -c -o simple.o -MMD -MF simple.deppy simple.c", ] end it "raises an error when given a source file with an unknown suffix" do test_dir("simple") result = run_test(rsconsfile: "error_unknown_suffix.rb") expect(result.stderr).to match /unknown input file type: "foo.xyz"/ end end context "SharedObject builder" do it "raises an error when given a source file with an unknown suffix" do test_dir("shared_library") result = run_test(rsconsfile: "error_unknown_suffix.rb") expect(result.stderr).to match /unknown input file type: "foo.xyz"/ end end context "Library builder" do it "allows overriding ARCMD construction variable" do test_dir("library") result = run_test(rsconsfile: "override_arcmd.rb") expect(result.stderr).to eq "" expect(lines(result.stdout)).to include "ar rcf lib.a build/one.o build/three.o build/two.o" end end context "SharedLibrary builder" do it "allows explicitly specifying SHLD construction variable value" do test_dir("shared_library") result = run_test(rsconsfile: "shared_library_set_shld.rb") expect(result.stderr).to eq "" slines = lines(result.stdout) if RUBY_PLATFORM =~ /mingw/ expect(slines).to include("SHLD mine.dll") else expect(slines).to include("SHLD libmine.so") end end end context "multi-threading" do it "waits for subcommands in threads for builders that support threaded commands" do test_dir("simple") start_time = Time.new result = run_test(rsconsfile: "threading.rb", rscons_args: %w[-j 4]) expect(result.stderr).to eq "" expect(Set[*lines(result.stdout)]).to eq Set[ "ThreadedTestBuilder a", "ThreadedTestBuilder b", "ThreadedTestBuilder c", "NonThreadedTestBuilder d", ] elapsed = Time.new - start_time expect(elapsed).to be < 4 end it "allows the user to specify that a target be built after another" do test_dir("custom_builder") result = run_test(rsconsfile: "build_after.rb", rscons_args: %w[-j 4]) expect(result.stderr).to eq "" end end context "CLI" do it "shows the version number and exits with --version argument" do test_dir("simple") result = run_test(rscons_args: %w[--version]) expect(result.stderr).to eq "" expect(result.status).to eq 0 expect(result.stdout).to match /version #{Rscons::VERSION}/ end it "shows CLI help and exits with --help argument" do test_dir("simple") result = run_test(rscons_args: %w[--help]) expect(result.stderr).to eq "" expect(result.status).to eq 0 expect(result.stdout).to match /Usage:/ end it "prints an error and exits with an error status when a default Rsconsfile cannot be found" do test_dir("simple") FileUtils.rm_f("Rsconsfile") result = run_test expect(result.stderr).to match /Could not find the Rsconsfile to execute/ expect(result.status).to_not eq 0 end it "prints an error and exits with an error status when the given Rsconsfile cannot be read" do test_dir("simple") result = run_test(rsconsfile: "nonexistent") expect(result.stderr).to match /Cannot read nonexistent/ expect(result.status).to_not eq 0 end end end