Skip to content

Commit

Permalink
Merge pull request #96 from Agile86/rust_basic_support_v2
Browse files Browse the repository at this point in the history
Rust tests
  • Loading branch information
generalmimon authored Sep 7, 2024
2 parents 0427d48 + 02cfdee commit a0bd64e
Show file tree
Hide file tree
Showing 378 changed files with 5,201 additions and 3,122 deletions.
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,12 @@ __pycache__/
_*
spec/nim/bin

# Rust compiled code, cargo and rls data
# Rust

## Cargo lockfile and build cache
spec/rust/Cargo.lock
spec/rust/target/

## Files auto-generated by builder/rust_builder.rb
spec/rust/tests/spec.rs
spec/rust/src/formats.rs
10 changes: 8 additions & 2 deletions aggregate/convert_to_json
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,19 @@ when 'java'
),
infile
)
when 'rust'
add_kst_adoption(
reports_to_h(
JUnitXMLParser.new("#{infile}/report.xml"),
BuildFailedParser.new("#{infile}/build_failed_tests.txt")
),
infile
)
when 'lua', 'nim'
add_kst_adoption(
reports_to_h(JUnitXMLParser.new("#{infile}/report.xml")),
infile
)
when 'rust'
reports_to_h(JUnitXMLParser.new(infile))
when 'go', 'perl', 'python', 'construct', 'julia'
add_kst_adoption(
reports_to_h(JUnitXMLParser.new("#{infile}/report.xml")),
Expand Down
3 changes: 3 additions & 0 deletions aggregate/junit_xml_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def each_test
elsif name =~ /^t(?!est)(.*?)$/
# Nim output
name = underscore_to_ucamelcase($1)
elsif name =~ /^test_.*::test_(.*?)$/
# Rust output
name = underscore_to_ucamelcase($1)
elsif tc.attribute('classname') and tc.attribute('classname').value =~ /^\/(.*?) test$/
# Julia output
name = $1
Expand Down
2 changes: 1 addition & 1 deletion builder/cpp_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def create_project(mand_files, disp_files)
(mand_files + disp_files).each { |l| f.puts(l) }
f.puts(")")
}
@disposable_cmake
[@disposable_cmake]
end

def build_project(log_file)
Expand Down
2 changes: 1 addition & 1 deletion builder/csharp_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def create_project(mand_files, disp_files)
files_xml = (mand_files + disp_files).map { |x| " <Compile Include=\"#{x}\" />" }.join("\n")
project = tmpl.gsub(/%%%FILES%%%/, files_xml)
File.write(@project_file, project)
@project_file
[@project_file]
end

def build_project(log_file)
Expand Down
2 changes: 1 addition & 1 deletion builder/java_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def create_project(mand_files, disp_files)
File.open(@project_file, 'w') { |f|
(mand_files + disp_files).each { |l| f.puts "\"#{l}\"" }
}
@project_file
[@project_file]
end

def build_project(log_file)
Expand Down
12 changes: 7 additions & 5 deletions builder/partial_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def command_line(arg)
l1 = list_mandatory_files
l2 = list_disposable_files
log "creating project with #{(l1 + l2).size} files"
fn = create_project(l1, l2)
log "project file created: #{fn.inspect}"
fns = create_project(l1, l2)
log "project files created: #{fns.inspect}"
elsif arg == ['--once']
@max_attempts = 1
exit run
Expand Down Expand Up @@ -78,8 +78,8 @@ def partial_build
attempt_str = @max_attempts ? "#{attempt}/#{@max_attempts}" : attempt

log "creating project with #{disp_files.size}/#{orig_size} files"
fn = create_project(mand_files, disp_files)
log "project file created: #{fn.inspect}"
fns = create_project(mand_files, disp_files)
log "project files created: #{fns.inspect}"

build_log = "#{@test_out_dir}/build-#{attempt}.log"
log "build attempt #{attempt_str} (log: #{build_log})"
Expand Down Expand Up @@ -215,7 +215,9 @@ def list_disposable_files

# Creates a project file, given a list of disposable and mandatory
# files to include in it.
# @return [String] project file name created
# @param mand_files [Enumerable<String>] collection of mandatory files
# @param disp_files [Enumerable<String>] collection of disposable files
# @return [Array<String>] project file names created
def create_project(mand_files, disp_files)
raise NotImplementedError
end
Expand Down
226 changes: 226 additions & 0 deletions builder/rust_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
require 'fileutils'
require 'set'
require 'json'

require_relative 'partial_builder'
require_relative 'shellconfig'

class RustBuilder < PartialBuilder
def initialize
super

base_spec_dir = 'spec/rust'
@base_spec_dir = File.absolute_path(base_spec_dir)

@spec_dir = File.join(@base_spec_dir, 'tests')
@formats_dir = File.join(@base_spec_dir, 'src/formats')
@spec_list_file = File.join(@base_spec_dir, 'tests/spec.rs')
@formats_list_file = File.join(@base_spec_dir, 'src/formats.rs')

test_out_dir = File.join(@config['TEST_OUT_DIR'], 'rust')
@test_out_dir = File.absolute_path(test_out_dir)
end

def list_mandatory_files
[]
end

def list_disposable_files
spec_basenames = Dir.glob('test_*.rs', base: @spec_dir)
format_basenames = Dir.glob('*.rs', base: @formats_dir)

spec_basenames.map { |fn| File.join(@spec_dir, fn) } +
format_basenames.map { |fn| File.join(@formats_dir, fn) }
end

def create_project(mand_files, disp_files)
grouped_files = mand_files.chain(disp_files).group_by { |fn| file_to_kind(fn) }
if grouped_files.key?(nil)
raise "unexpected files that are neither ':spec' or ':format': #{grouped_files[nil].inspect}"
end

File.open(@spec_list_file, 'w') { |f|
f.puts '#![allow(unused_variables)]'
f.puts '#![allow(unused_assignments)]'
grouped_files[:spec].each { |fn| f.puts "pub mod #{File.basename(fn, '.rs')};" }
}
File.open(@formats_list_file, 'w') { |f|
f.puts '#![allow(unused_parens)]'
f.puts '#![allow(dead_code)]'
grouped_files[:format].each { |fn| f.puts "pub mod #{File.basename(fn, '.rs')};" }
}
[@spec_list_file, @formats_list_file]
end

def build_project(log_file)
Dir.chdir(@base_spec_dir) do
# We don't use `cargo check` here (which would seem like a more logical
# choice) because unfortunately it doesn't report all build errors, see
# https://doc.rust-lang.org/cargo/commands/cargo-check.html#description:
#
# > Some diagnostics and errors are only emitted during code generation,
# > so they inherently won't be reported with `cargo check`.
cli = %w[cargo test --no-run --test spec --message-format json]
run_cargo_build({}, cli, log_file).exitstatus
end
end

def parse_failed_build(log_file)
list = []

File.open(log_file, 'r') { |f|
f.each_line { |line|
line.chomp!
# See https://doc.rust-lang.org/cargo/reference/external-tools.html#json-messages
cargo_msg = JSON.parse(line)

# See https://doc.rust-lang.org/cargo/reference/external-tools.html#build-finished:
# > The "build-finished" message is emitted at the end of the build.
#
# > This message can be helpful for tools to know when to stop reading JSON messages.
break if cargo_msg['reason'] == 'build-finished'

next unless cargo_msg['reason'] == 'compiler-message'

# See https://doc.rust-lang.org/rustc/json.html
rustc_msg = cargo_msg['message']
next unless rustc_msg['$message_type'] == 'diagnostic'
next unless rustc_msg['level'] == 'error'

files =
select_rustc_msg_culprit_spans(rustc_msg['spans'])
.map { |span| File.absolute_path(span['file_name'], @base_spec_dir) }

list.concat(files)
}
}

list
end

def file_to_test(path)
kind = file_to_kind(path)
basename = File.basename(path, '.rs')
test_name = kind == :spec ? basename.delete_prefix('test_') : basename
[kind, underscore_to_ucamelcase(test_name)]
end

def run_tests
Dir.chdir(@base_spec_dir) do
cli = %w[cargo nextest run --test spec]
out_log = File.join(@test_out_dir, 'test_run.stdout')
run_and_tee({}, cli, out_log)

# See spec/rust/.config/nextest.toml
src_path = File.join(@base_spec_dir, 'target/nextest/default/junit.xml')
dest_path = File.join(@test_out_dir, 'report.xml')
FileUtils.copy_file(src_path, dest_path)
true
end
end

private

def select_rustc_msg_culprit_spans(spans)
primary_spans =
spans
.select { |span| span['is_primary'] }
.map do |span|
span = span['expansion']['span'] until span['expansion'].nil?
span
end
primary_spans.uniq { |span| span['file_name'] }
end

def file_to_kind(path)
if path_directly_in_dir?(path, @spec_dir)
:spec
elsif path_directly_in_dir?(path, @formats_dir)
:format
end
end

def run_cargo_build(environment, cmd, stdout_file)
log "running command: #{cmd.inspect}, log: #{stdout_file.inspect}"
process_status = nil
FileUtils.mkdir_p(File.dirname(stdout_file))
File.open(stdout_file, 'w') { |f|
Open3.popen3(environment, *cmd) { |_stdin, stdout, _stderr, wait_thread|
while (line = stdout.gets)
line.chomp!
line_summary = summarize_cargo_json_line(line)
puts line_summary unless line_summary.nil?
f.puts line
end
process_status = wait_thread.value
}
}
log "process_status: #{process_status.inspect}"
process_status
end

def summarize_cargo_json_line(line)
begin
# See https://doc.rust-lang.org/cargo/reference/external-tools.html#json-messages
cargo_msg = JSON.parse(line)
rescue JSON::ParserError => e
warn e.full_message
return line
end
reason = cargo_msg['reason']

case reason
when 'compiler-message'
# See https://doc.rust-lang.org/rustc/json.html
rustc_msg = cargo_msg['message']

if rustc_msg['$message_type'] == 'diagnostic'
files =
select_rustc_msg_culprit_spans(rustc_msg['spans'])
.map { |span| "#{span['file_name']}:#{span['line_start']}:#{span['column_start']}" }
code_appendix = rustc_msg['code'].nil? ? '' : "[#{rustc_msg['code']['code']}]"
files_prefix =
if files.empty?
''
else
"#{files.join(', ')}: "
end
"#{files_prefix}#{rustc_msg['level']}#{code_appendix}: #{rustc_msg['message']}"
else
line
end
when 'compiler-artifact', 'build-script-executed'
# "[#{reason}] #{cargo_msg['package_id']}"
nil
when 'build-finished'
"[#{reason}] success: #{cargo_msg['success']}"
else
line
end
end

# Returns +true+ if +path+ is directly contained in the +dir_abs_path+ directory, and
# +false+ otherwise.
#
# The +dir_abs_path+ argument is expected to be normalized as if returned by
# the +File.absolute_path+ method, i.e. directory parts in path separated only
# by +File::SEPARATOR+ (forward slash +/+ on all platforms) and never
# +File::ALT_SEPARATOR+ (backslash +\+ on Windows), multiple consecutive
# slashes collapsed into one, no useless dots (+./+ or +../+ parts), no
# trailing slash, etc.
# @param path [String] relative (to the current directory) or absolute path
# @param dir_abs_path [String] normalized absolute path of the directory
def path_directly_in_dir?(path, dir_abs_path)
abs_path = File.absolute_path(path)
dir_abs_path += File::SEPARATOR
return false unless abs_path.start_with?(dir_abs_path)

path_rel_to_dir = abs_path.delete_prefix(dir_abs_path)
path_basename = File.basename(abs_path)
path_rel_to_dir == path_basename
end

def underscore_to_ucamelcase(s)
s.split(/_/).map { |x| x.capitalize }.join
end
end
3 changes: 3 additions & 0 deletions builder/spec/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Cargo lockfile and build cache
*/spec/rust/Cargo.lock
*/spec/rust/target/
1 change: 1 addition & 0 deletions builder/spec/rust/macro_expansion/config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST_OUT_DIR=test_out
9 changes: 9 additions & 0 deletions builder/spec/rust/macro_expansion/spec/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "rust"
edition = "2018"
autotests = false

[dependencies]

[[test]]
name = "spec"
3 changes: 3 additions & 0 deletions builder/spec/rust/macro_expansion/spec/rust/src/formats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#![allow(unused_parens)]
#![allow(dead_code)]
pub mod empty;
Empty file.
2 changes: 2 additions & 0 deletions builder/spec/rust/macro_expansion/spec/rust/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[macro_export]
macro_rules! our_assert_eq_wrapper { ($l:expr, $r:expr) => { assert_eq!($l, $r) } }
2 changes: 2 additions & 0 deletions builder/spec/rust/macro_expansion/spec/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod formats;
mod helpers;
1 change: 1 addition & 0 deletions builder/spec/rust/macro_expansion/spec/rust/tests/spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod test_nested;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use rust::our_assert_eq_wrapper;

#[test]
fn test_nested() {
// NOTE: This line intentionally triggers a "can't compare `{integer}` with `{float}`"
// error to check if RustBuilder recovers from it.
our_assert_eq_wrapper!(4, 2.5);
}
Loading

0 comments on commit a0bd64e

Please sign in to comment.