Merge pull request #4802 from roc-lang/valgrind-unit-tests

Valgrind unit tests
This commit is contained in:
Folkert de Vries 2023-01-11 19:45:28 +01:00 committed by GitHub
commit d8b2ff07f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 547 additions and 64 deletions

18
Cargo.lock generated
View file

@ -5262,6 +5262,24 @@ dependencies = [
"getrandom",
]
[[package]]
name = "valgrind"
version = "0.1.0"
dependencies = [
"bumpalo",
"cli_utils",
"indoc",
"roc_build",
"roc_cli",
"roc_linker",
"roc_load",
"roc_mono",
"roc_packaging",
"roc_reporting",
"target-lexicon",
"tempfile",
]
[[package]]
name = "valuable"
version = "0.1.0"

View file

@ -17,6 +17,7 @@ members = [
"crates/repl_wasm",
"crates/repl_expect",
"crates/test_utils",
"crates/valgrind",
"crates/tracing",
"crates/utils",
"crates/docs",
@ -129,3 +130,7 @@ codegen-units = 1
[profile.bench]
lto = "thin"
codegen-units = 1
[profile.release-with-debug]
inherits = "release"
debug = true

View file

@ -4,7 +4,7 @@ use roc_build::{
legacy_host_filename, link, preprocess_host_wasm32, preprocessed_host_filename,
rebuild_host, LinkType, LinkingStrategy,
},
program::{self, CodeGenOptions},
program::{self, CodeGenBackend, CodeGenOptions},
};
use roc_builtins::bitcode;
use roc_load::{
@ -18,7 +18,10 @@ use roc_reporting::{
report::{RenderTarget, DEFAULT_PALETTE},
};
use roc_target::TargetInfo;
use std::time::{Duration, Instant};
use std::{
path::Path,
time::{Duration, Instant},
};
use std::{path::PathBuf, thread::JoinHandle};
use target_lexicon::Triple;
@ -570,3 +573,56 @@ pub fn check_file<'a>(
compilation_end,
))
}
pub fn build_str_test<'a>(
arena: &'a Bump,
app_module_path: &Path,
app_module_source: &'a str,
assume_prebuild: bool,
) -> Result<BuiltFile<'a>, BuildFileError<'a>> {
let triple = target_lexicon::Triple::host();
let code_gen_options = CodeGenOptions {
backend: CodeGenBackend::Llvm,
opt_level: OptLevel::Normal,
emit_debug_info: false,
};
let emit_timings = false;
let link_type = LinkType::Executable;
let linking_strategy = LinkingStrategy::Surgical;
let wasm_dev_stack_bytes = None;
let roc_cache_dir = roc_packaging::cache::RocCacheDir::Disallowed;
let build_ordering = BuildOrdering::AlwaysBuild;
let threading = Threading::AtMost(2);
let load_config = standard_load_config(&triple, build_ordering, threading);
let compilation_start = std::time::Instant::now();
// Step 1: compile the app and generate the .o file
let loaded = roc_load::load_and_monomorphize_from_str(
arena,
PathBuf::from("valgrind_test.roc"),
app_module_source,
app_module_path.to_path_buf(),
roc_cache_dir,
load_config,
)
.map_err(|e| BuildFileError::from_mono_error(e, compilation_start))?;
build_loaded_file(
arena,
&triple,
app_module_path.to_path_buf(),
code_gen_options,
emit_timings,
link_type,
linking_strategy,
assume_prebuild,
wasm_dev_stack_bytes,
loaded,
compilation_start,
)
}

View file

@ -13,7 +13,6 @@ use roc_load::{ExpectMetadata, LoadingProblem, Threading};
use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir;
use roc_packaging::tarball::Compression;
use roc_reporting::cli::Problems;
use std::env;
use std::ffi::{CString, OsStr};
use std::io;
@ -463,7 +462,7 @@ pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
"if there were errors, we would have already exited."
);
if problems.warnings > 0 {
print_problems(problems, start_time.elapsed());
problems.print_to_stdout(start_time.elapsed());
println!(".\n\nRunning tests…\n\n\x1B[36m{}\x1B[39m", "".repeat(80));
}
}
@ -708,7 +707,7 @@ pub fn build(
// since the process is about to exit anyway.
// std::mem::forget(arena);
print_problems(problems, total_time);
problems.print_to_stdout(total_time);
println!(" while successfully building:\n\n {generated_filename}");
// Return a nonzero exit code if there were problems
@ -716,7 +715,7 @@ pub fn build(
}
BuildAndRun => {
if problems.errors > 0 || problems.warnings > 0 {
print_problems(problems, total_time);
problems.print_to_stdout(total_time);
println!(
".\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
@ -737,7 +736,7 @@ pub fn build(
"if there are errors, they should have been returned as an error variant"
);
if problems.warnings > 0 {
print_problems(problems, total_time);
problems.print_to_stdout(total_time);
println!(
".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
@ -771,7 +770,7 @@ fn handle_error_module(
let problems = roc_build::program::report_problems_typechecked(&mut module);
print_problems(problems, total_time);
problems.print_to_stdout(total_time);
if print_run_anyway_hint {
// If you're running "main.roc" then you can just do `roc run`
@ -803,34 +802,6 @@ fn handle_loading_problem(problem: LoadingProblem) -> io::Result<i32> {
}
}
fn print_problems(problems: Problems, total_time: std::time::Duration) {
const GREEN: usize = 32;
const YELLOW: usize = 33;
print!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms",
match problems.errors {
0 => GREEN,
_ => YELLOW,
},
problems.errors,
match problems.errors {
1 => "error",
_ => "errors",
},
match problems.warnings {
0 => GREEN,
_ => YELLOW,
},
problems.warnings,
match problems.warnings {
1 => "warning",
_ => "warnings",
},
total_time.as_millis(),
);
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump,
opt_level: OptLevel,

View file

@ -251,10 +251,10 @@ pub fn run_with_valgrind<'a, I: IntoIterator<Item = &'a str>>(
let mut cmd = Command::new("valgrind");
let named_tempfile =
NamedTempFile::new().expect("Unable to create tempfile for valgrind results");
let filepath = named_tempfile.path().to_str().unwrap();
cmd.arg("--tool=memcheck");
cmd.arg("--xml=yes");
cmd.arg(format!("--xml-file={}", named_tempfile.path().display()));
// If you are having valgrind issues on MacOS, you may need to suppress some
// of the errors. Read more here: https://github.com/roc-lang/roc/issues/746
@ -274,8 +274,6 @@ pub fn run_with_valgrind<'a, I: IntoIterator<Item = &'a str>>(
}
}
cmd.arg(format!("--xml-file={}", filepath));
for arg in args {
cmd.arg(arg);
}

View file

@ -4468,23 +4468,7 @@ fn build_header<'a>(
//
// Also build a list of imported_values_to_expose (like `bar` above.)
for (qualified_module_name, exposed_idents, region) in imported.into_iter() {
let cloned_module_name = qualified_module_name.module.clone();
let pq_module_name = if qualified_module_name.is_builtin() {
// If this is a builtin, it must be unqualified, and we should *never* prefix it
// with the package shorthand! The user intended to import the module as-is here.
debug_assert!(qualified_module_name.opt_package.is_none());
PQModuleName::Unqualified(qualified_module_name.module)
} else {
match qualified_module_name.opt_package {
None => match opt_shorthand {
Some(shorthand) => {
PQModuleName::Qualified(shorthand, qualified_module_name.module)
}
None => PQModuleName::Unqualified(qualified_module_name.module),
},
Some(package) => PQModuleName::Qualified(package, cloned_module_name),
}
};
let pq_module_name = qualified_module_name.into_pq_module_name(opt_shorthand);
let module_id = module_ids.get_or_insert(&pq_module_name);
@ -4497,18 +4481,14 @@ fn build_header<'a>(
// to the same symbols as the ones we're using here.
let ident_ids = ident_ids_by_module.get_or_insert(module_id);
for Loc {
region,
value: ident,
} in exposed_idents
{
let ident_id = ident_ids.get_or_insert(ident.as_str());
for loc_ident in exposed_idents {
let ident_id = ident_ids.get_or_insert(loc_ident.value.as_str());
let symbol = Symbol::new(module_id, ident_id);
// Since this value is exposed, add it to our module's default scope.
debug_assert!(!scope.contains_key(&ident));
debug_assert!(!scope.contains_key(&loc_ident.value));
scope.insert(ident, (symbol, region));
scope.insert(loc_ident.value, (symbol, loc_ident.region));
}
}

View file

@ -1,6 +1,8 @@
pub use roc_ident::IdentStr;
use std::fmt;
use crate::symbol::PQModuleName;
/// This could be uppercase or lowercase, qualified or unqualified.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct Ident(pub IdentStr);
@ -21,6 +23,25 @@ pub struct QualifiedModuleName<'a> {
pub module: ModuleName,
}
impl<'a> QualifiedModuleName<'a> {
pub fn into_pq_module_name(self, opt_shorthand: Option<&'a str>) -> PQModuleName<'a> {
if self.is_builtin() {
// If this is a builtin, it must be unqualified, and we should *never* prefix it
// with the package shorthand! The user intended to import the module as-is here.
debug_assert!(self.opt_package.is_none());
PQModuleName::Unqualified(self.module)
} else {
match self.opt_package {
None => match opt_shorthand {
Some(shorthand) => PQModuleName::Qualified(shorthand, self.module),
None => PQModuleName::Unqualified(self.module),
},
Some(package) => PQModuleName::Qualified(package, self.module),
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModuleName(IdentStr);

View file

@ -20,6 +20,34 @@ impl Problems {
self.warnings.min(1) as i32
}
}
pub fn print_to_stdout(&self, total_time: std::time::Duration) {
const GREEN: usize = 32;
const YELLOW: usize = 33;
print!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms",
match self.errors {
0 => GREEN,
_ => YELLOW,
},
self.errors,
match self.errors {
1 => "error",
_ => "errors",
},
match self.warnings {
0 => GREEN,
_ => YELLOW,
},
self.warnings,
match self.warnings {
1 => "warning",
_ => "warnings",
},
total_time.as_millis(),
);
}
}
pub fn report_problems(

View file

@ -0,0 +1,20 @@
[package]
name = "valgrind"
version = "0.1.0"
edition = "2021"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
[dev-dependencies]
roc_cli = { path = "../cli" }
cli_utils = { path = "../cli_utils" }
roc_build = { path = "../compiler/build" }
roc_mono = { path = "../compiler/mono" }
roc_load = { path = "../compiler/load" }
roc_reporting = { path = "../reporting" }
roc_linker = { path = "../linker" }
roc_packaging = { path = "../packaging" }
bumpalo.workspace = true
target-lexicon.workspace = true
tempfile.workspace = true
indoc.workspace = true

237
crates/valgrind/src/lib.rs Normal file
View file

@ -0,0 +1,237 @@
#![cfg(test)]
use indoc::indoc;
#[cfg(target_os = "linux")]
static BUILD_ONCE: std::sync::Once = std::sync::Once::new();
#[cfg(all(target_os = "linux"))]
fn build_host() {
use roc_build::link::preprocessed_host_filename;
use roc_linker::build_and_preprocess_host;
let platform_main_roc = std::env::current_dir()
.unwrap()
.join("zig-platform/main.roc");
// tests always run on the host
let target = target_lexicon::Triple::host();
// the preprocessed host is stored beside the platform's main.roc
let preprocessed_host_path =
platform_main_roc.with_file_name(preprocessed_host_filename(&target).unwrap());
build_and_preprocess_host(
roc_mono::ir::OptLevel::Normal,
&target,
&platform_main_roc,
&preprocessed_host_path,
vec![String::from("mainForHost")],
vec![],
);
}
fn valgrind_test(source: &str) {
#[cfg(target_os = "linux")]
{
valgrind_test_linux(source)
}
#[cfg(not(target_os = "linux"))]
{
let _ = source;
}
}
#[cfg(target_os = "linux")]
fn valgrind_test_linux(source: &str) {
use roc_cli::build::BuiltFile;
// the host is identical for all tests so we only want to build it once
BUILD_ONCE.call_once(build_host);
let pf = std::env::current_dir()
.unwrap()
.join("zig-platform/main.roc");
assert!(pf.exists(), "cannot find platform {:?}", &pf);
let mut app_module_source = format!(
indoc::indoc!(
r#"
app "test"
packages {{ pf: "{}" }}
imports []
provides [main] to pf
main =
"#
),
pf.to_str().unwrap()
);
for line in source.lines() {
app_module_source.push_str(" ");
app_module_source.push_str(line);
app_module_source.push('\n');
}
let temp_dir = tempfile::tempdir().unwrap();
let app_module_path = temp_dir.path().join("app.roc");
let arena = bumpalo::Bump::new();
let assume_prebuilt = true;
let res_binary_path = roc_cli::build::build_str_test(
&arena,
&app_module_path,
&app_module_source,
assume_prebuilt,
);
match res_binary_path {
Ok(BuiltFile {
binary_path,
problems,
total_time: _,
expect_metadata: _,
}) => {
if problems.exit_code() != 0 {
panic!("there are problems")
}
run_with_valgrind(&binary_path);
}
Err(roc_cli::build::BuildFileError::LoadingProblem(
roc_load::LoadingProblem::FormattedReport(report),
)) => {
eprintln!("{}", report);
panic!("");
}
Err(e) => panic!("{:?}", e),
}
drop(temp_dir)
}
#[allow(unused)]
fn run_with_valgrind(binary_path: &std::path::Path) {
use cli_utils::helpers::{extract_valgrind_errors, ValgrindError, ValgrindErrorXWhat};
// If possible, report the generated executable name relative to the current dir.
let generated_filename = binary_path
.strip_prefix(std::env::current_dir().unwrap())
.unwrap_or(binary_path)
.to_str()
.unwrap();
let (valgrind_out, raw_xml) =
cli_utils::helpers::run_with_valgrind([], &[generated_filename.to_string()]);
if valgrind_out.status.success() {
let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| {
panic!(
indoc!(
r#"
failed to parse the `valgrind` xml output:
Error was:
{:?}
valgrind xml was:
{}
valgrind stdout was:
{}
valgrind stderr was:
{}
"#
),
err, raw_xml, valgrind_out.stdout, valgrind_out.stderr
);
});
if !memory_errors.is_empty() {
for error in memory_errors {
let ValgrindError {
kind,
what: _,
xwhat,
} = error;
println!("Valgrind Error: {}\n", kind);
if let Some(ValgrindErrorXWhat {
text,
leakedbytes: _,
leakedblocks: _,
}) = xwhat
{
println!(" {}", text);
}
}
panic!("Valgrind found memory errors");
}
} else {
let exit_code = match valgrind_out.status.code() {
Some(code) => format!("exit code {}", code),
None => "no exit code".to_string(),
};
panic!(
"`valgrind` exited with {}. valgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"",
exit_code, valgrind_out.stdout, valgrind_out.stderr
);
}
}
#[test]
fn list_concat_consumes_first_argument() {
valgrind_test("List.concat (List.withCapacity 1024) [1,2,3] |> List.len |> Num.toStr");
}
#[test]
fn str_capacity_concat() {
valgrind_test(r#"Str.withCapacity 42 |> Str.concat "foobar""#);
}
#[test]
fn append_scalar() {
valgrind_test(indoc!(
r#"
Str.appendScalar "abcd" 'A'
|> Result.withDefault ""
"#
));
}
#[test]
fn split_not_present() {
valgrind_test(indoc!(
r#"
Str.split (Str.concat "a string that is stored on the heap" "!") "\n"
|> Str.joinWith ""
"#
));
}
#[test]
fn str_concat_first_argument_not_unique() {
valgrind_test(indoc!(
r#"
(
str1 = Str.reserve "" 48
str2 = "a"
out = Str.concat str1 str2
if Bool.false then
out
else
str1
)
"#
));
}

View file

@ -0,0 +1,2 @@
dynhost
libapp.so

View file

@ -0,0 +1,138 @@
const std = @import("std");
const builtin = @import("builtin");
const str = @import("str");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
comptime {
// This is a workaround for https://github.com/ziglang/zig/issues/8218
// which is only necessary on macOS.
//
// Once that issue is fixed, we can undo the changes in
// 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing
// -fcompiler-rt in link.rs instead of doing this. Note that this
// workaround is present in many host.zig files, so make sure to undo
// it everywhere!
if (builtin.os.tag == .macos) {
_ = @import("compiler_rt");
}
}
const Align = 2 * @alignOf(usize);
extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque;
extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque;
extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void;
extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void;
extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void;
const DEBUG: bool = false;
export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
var ptr = malloc(size);
const stdout = std.io.getStdOut().writer();
stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable;
return ptr;
} else {
return malloc(size);
}
}
export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable;
}
return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size);
}
export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable;
}
free(@alignCast(Align, @ptrCast([*]u8, c_ptr)));
}
export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void {
_ = tag_id;
const stderr = std.io.getStdErr().writer();
const msg = @ptrCast([*:0]const u8, c_ptr);
stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable;
std.process.exit(0);
}
export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void {
return memcpy(dst, src, size);
}
export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void {
return memset(dst, value, size);
}
extern fn kill(pid: c_int, sig: c_int) c_int;
extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int;
extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque;
extern fn getppid() c_int;
fn roc_getppid() callconv(.C) c_int {
return getppid();
}
fn roc_getppid_windows_stub() callconv(.C) c_int {
return 0;
}
fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int {
return shm_open(name, oflag, mode);
}
fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque {
return mmap(addr, length, prot, flags, fd, offset);
}
comptime {
if (builtin.os.tag == .macos or builtin.os.tag == .linux) {
@export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong });
@export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong });
@export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong });
}
if (builtin.os.tag == .windows) {
@export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong });
}
}
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed_generic(*RocStr) void;
const Unit = extern struct {};
pub fn main() u8 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
var timer = std.time.Timer.start() catch unreachable;
// actually call roc to populate the callresult
var callresult = RocStr.empty();
roc__mainForHost_1_exposed_generic(&callresult);
const nanos = timer.read();
const seconds = (@intToFloat(f64, nanos) / 1_000_000_000.0);
// stdout the result
stdout.print("{s}", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;
return 0;
}

View file

@ -0,0 +1,9 @@
platform "echo-in-zig"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main