Implement collection of source line coverage (#1518)

Implement source line coverage recording for both Linux and Windows.
This commit is contained in:
Joe Ranweiler
2021-12-14 08:33:41 -08:00
committed by GitHub
parent 7cabc6b924
commit 03dba6f163
7 changed files with 620 additions and 2 deletions

288
src/agent/Cargo.lock generated
View File

@ -71,6 +71,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "async-channel"
version = "1.6.1"
@ -213,6 +219,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brownstone"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030ea61398f34f1395ccbeb046fb68c87b631d1f34567fed0f0f11fa35d18d8d"
dependencies = [
"arrayvec 0.7.2",
]
[[package]]
name = "bumpalo"
version = "3.7.1"
@ -240,6 +255,27 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cache-padded"
version = "1.1.1"
@ -368,6 +404,7 @@ dependencies = [
"serde",
"serde_json",
"structopt",
"symbolic",
"uuid",
"win-util",
"winapi 0.3.9",
@ -563,6 +600,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "debugid"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91cf5a8c2f2097e2a32627123508635d47ce10563d999ec1a95addf08b502ba"
dependencies = [
"uuid",
]
[[package]]
name = "derivative"
version = "2.2.0"
@ -591,6 +637,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "dmsort"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4699f5cb7678f099b747ffdc1e6ce6cdc42579e29d28315aa715b9fd10324fc"
[[package]]
name = "doc-comment"
version = "0.3.3"
@ -615,6 +667,16 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "elementtree"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19c5d32d0ab83734d2d7452047ef901c105991044b7b07da30fe82371a149a25"
dependencies = [
"string_cache",
"xml-rs",
]
[[package]]
name = "encoding_rs"
version = "0.8.28"
@ -974,6 +1036,10 @@ name = "gimli"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
dependencies = [
"fallible-iterator",
"stable_deref_trait",
]
[[package]]
name = "glob"
@ -1160,6 +1226,12 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "id-arena"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "idna"
version = "0.2.3"
@ -1171,6 +1243,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indent_write"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3"
[[package]]
name = "indexmap"
version = "1.7.0"
@ -1255,6 +1333,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "joinery"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
[[package]]
name = "js-sys"
version = "0.3.55"
@ -1286,13 +1370,19 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leb128"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"arrayvec 0.5.2",
"bitflags",
"cfg-if 1.0.0",
"ryu",
@ -1379,6 +1469,16 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "memmap2"
version = "0.5.0"
@ -1513,6 +1613,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.23.0"
@ -1560,6 +1666,19 @@ dependencies = [
"version_check",
]
[[package]]
name = "nom-supreme"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aadc66631948f6b65da03be4c4cd8bd104d481697ecbb9bbd65719b1ec60bc9f"
dependencies = [
"brownstone",
"indent_write",
"joinery",
"memchr",
"nom 7.1.0",
]
[[package]]
name = "notify"
version = "4.0.17"
@ -1871,6 +1990,15 @@ dependencies = [
"thiserror",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.0.8"
@ -1921,6 +2049,12 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty_assertions"
version = "1.0.0"
@ -2472,6 +2606,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]]
name = "slab"
version = "0.4.4"
@ -2565,6 +2705,12 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacktrace-parser"
version = "0.1.0"
@ -2609,6 +2755,20 @@ dependencies = [
"uuid",
]
[[package]]
name = "string_cache"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
"serde",
]
[[package]]
name = "strsim"
version = "0.8.0"
@ -2658,6 +2818,86 @@ dependencies = [
"syn 1.0.76",
]
[[package]]
name = "symbolic"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0b3be4c272aaef995fca595a89355caf49993b84a964013e4a94447f13c779"
dependencies = [
"symbolic-common",
"symbolic-debuginfo",
"symbolic-demangle",
"symbolic-symcache",
]
[[package]]
name = "symbolic-common"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfc8618f0f31ed048f8e66aa2caecedfbdbbca962ff9ad87107ba4171de0742b"
dependencies = [
"debugid",
"memmap",
"stable_deref_trait",
"uuid",
]
[[package]]
name = "symbolic-debuginfo"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac34b5f005e9c87aa9c60a712102f80dca51cd9edf1fa04d5c7392b936c45d06"
dependencies = [
"dmsort",
"elementtree",
"fallible-iterator",
"flate2",
"gimli",
"goblin",
"lazy_static",
"lazycell",
"nom 7.1.0",
"nom-supreme",
"parking_lot",
"pdb",
"regex",
"scroll",
"serde",
"serde_json",
"smallvec",
"symbolic-common",
"thiserror",
"walrus",
"wasmparser",
"zip",
]
[[package]]
name = "symbolic-demangle"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be790f170c892899802aa1d382b7b5b65baf692b1357864c74e92bbbbdabfbe"
dependencies = [
"cc",
"cpp_demangle",
"msvc-demangler",
"rustc-demangle",
"symbolic-common",
]
[[package]]
name = "symbolic-symcache"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35cdeb276d2476e76b3ce365d3e812a0df5ab04ef4c2781b6f4a962e22337c6"
dependencies = [
"dmsort",
"fnv",
"symbolic-common",
"symbolic-debuginfo",
"thiserror",
]
[[package]]
name = "syn"
version = "0.15.44"
@ -3034,6 +3274,32 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "walrus"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb08e48cde54c05f363d984bb54ce374f49e242def9468d2e1b6c2372d291f8"
dependencies = [
"anyhow",
"id-arena",
"leb128",
"log",
"walrus-macro",
"wasmparser",
]
[[package]]
name = "walrus-macro"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7"
dependencies = [
"heck",
"proc-macro2 1.0.29",
"quote 1.0.9",
"syn 1.0.76",
]
[[package]]
name = "want"
version = "0.3.0"
@ -3122,6 +3388,12 @@ version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "wasmparser"
version = "0.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6"
[[package]]
name = "web-sys"
version = "0.3.55"
@ -3290,3 +3562,17 @@ name = "z3-sys"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa18ba5fbd4933e41ffb440c3fd91f91fe9cdb7310cce3ddfb6648563811de0"
[[package]]
name = "zip"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time",
]

View File

@ -24,6 +24,7 @@ msvc-demangler = "0.9"
regex = "1.4"
rustc-demangle = "0.1"
serde = { version = "1.0", features = ["derive"] }
symbolic = { version = "8.5", features = ["debuginfo", "demangle", "symcache"] }
uuid = { version = "0.8", features = ["guid"] }
win-util = { path = "../win-util" }

View File

@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use std::{process::Command, process::Stdio};
use anyhow::Result;
use coverage::block::CommandBlockCov as Coverage;
use coverage::cache::ModuleCache;
use coverage::code::CmdFilter;
use structopt::StructOpt;
#[derive(Debug, PartialEq, StructOpt)]
struct Opt {
#[structopt(short, long, min_values = 1)]
inputs: Vec<PathBuf>,
#[structopt(short, long)]
dir: Option<PathBuf>,
#[structopt(min_values = 2)]
cmd: Vec<String>,
#[structopt(short, long, long_help = "Timeout in ms", default_value = "5000")]
timeout: u64,
}
fn main() -> Result<()> {
let opt = Opt::from_args();
let filter = CmdFilter::default();
let mut cache = ModuleCache::default();
let mut total = Coverage::default();
let timeout = Duration::from_millis(opt.timeout);
if let Some(dir) = &opt.dir {
for entry in std::fs::read_dir(dir)? {
let input = entry?.path();
println!("testing input: {}", input.display());
let cmd = input_command(&opt.cmd, &input);
let coverage = record(&mut cache, filter.clone(), cmd, timeout)?;
total.merge_max(&coverage);
}
}
for input in &opt.inputs {
println!("testing input: {}", input.display());
let cmd = input_command(&opt.cmd, input);
let coverage = record(&mut cache, filter.clone(), cmd, timeout)?;
total.merge_max(&coverage);
}
let mut debug_info = coverage::debuginfo::DebugInfo::default();
let src_coverage = total.source_coverage(&mut debug_info)?;
for file_coverage in src_coverage.files {
for location in &file_coverage.locations {
println!(
"{} {}:{}",
location.count, file_coverage.file, location.line
);
}
}
Ok(())
}
fn input_command(argv: &[String], input: &Path) -> Command {
let mut cmd = Command::new(&argv[0]);
cmd.stdin(Stdio::null());
cmd.stderr(Stdio::null());
cmd.stdout(Stdio::null());
let args: Vec<_> = argv[1..]
.iter()
.map(|a| {
if &*a == "@@" {
input.display().to_string()
} else {
a.to_string()
}
})
.collect();
cmd.args(&args);
cmd
}
#[cfg(target_os = "macos")]
fn record(
_cache: &mut ModuleCache,
_filter: CmdFilter,
_cmd: Command,
_timeout: Duration,
) -> Result<Coverage> {
unimplemented!("coverage recording is not supported on macOS");
}
#[cfg(target_os = "linux")]
fn record(
cache: &mut ModuleCache,
filter: CmdFilter,
cmd: Command,
timeout: Duration,
) -> Result<Coverage> {
use coverage::block::linux::Recorder;
let now = Instant::now();
let coverage = Recorder::record(cmd, timeout, cache, filter.clone())?;
let elapsed = now.elapsed();
log::info!("recorded in {:?}", elapsed);
Ok(coverage)
}
#[cfg(target_os = "windows")]
fn record(
cache: &mut ModuleCache,
filter: CmdFilter,
cmd: Command,
timeout: Duration,
) -> Result<Coverage> {
use coverage::block::windows::{Recorder, RecorderEventHandler};
let mut recorder = Recorder::new(cache, filter);
let mut handler = RecorderEventHandler::new(&mut recorder, timeout);
let now = Instant::now();
handler.run(cmd)?;
let elapsed = now.elapsed();
log::info!("recorded in {:?}", elapsed);
Ok(recorder.into_coverage())
}

View File

@ -17,7 +17,9 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::code::ModulePath;
use crate::debuginfo::DebugInfo;
use crate::report::{CoverageReport, CoverageReportEntry};
use crate::source::SourceCoverage;
/// Block coverage for a command invocation.
///
@ -104,6 +106,70 @@ impl CommandBlockCov {
pub fn try_from_report(report: BlockCoverageReport) -> Result<Self> {
Self::try_from(report)
}
/// Translate binary block coverage to source line coverage, using a caching
/// debug info provider.
pub fn source_coverage(&self, debuginfo: &mut DebugInfo) -> Result<SourceCoverage> {
use crate::source::{SourceCoverageLocation as Location, *};
use std::collections::HashMap;
// Temporary map to collect line coverage results without duplication.
// Will be converted after processing block coverage.
//
// Maps: source_file_path -> (line -> count)
let mut files: HashMap<String, HashMap<u32, u32>> = HashMap::default();
for (module, coverage) in &self.modules {
let loaded = debuginfo.load_module(module.path().to_owned())?;
if !loaded {
continue;
}
let mod_info = debuginfo.get(&module.path());
if let Some(mod_info) = mod_info {
for (offset, block) in &coverage.blocks {
let lines = mod_info.source.lookup(u64::from(*offset))?;
for line_info in lines {
let line_info = line_info?;
let file = line_info.path().to_owned();
let line = line_info.line();
let file_entry = files.entry(file).or_default();
let line_entry = file_entry.entry(line).or_insert(0);
// Will always be 0 or 1.
*line_entry = u32::max(*line_entry, block.count);
}
}
}
}
let mut src = SourceCoverage::default();
for (file, lines) in files {
let mut locations = vec![];
for (line, count) in lines {
// Valid lines are always 1-indexed.
if line > 0 {
let location = Location::new(line, None, count)?;
locations.push(location)
}
}
locations.sort_unstable_by_key(|l| l.line);
let file_coverage = SourceFileCoverage { file, locations };
src.files.push(file_coverage);
}
src.files.sort_unstable_by_key(|f| f.file.clone());
Ok(src)
}
}
impl From<CommandBlockCov> for BlockCoverageReport {

View File

@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use anyhow::Result;
use symbolic::{
debuginfo::{pe, Object, ObjectDebugSession},
symcache::{SymCache, SymCacheWriter},
};
/// Caching provider of debug info for executable code modules.
#[derive(Default)]
pub struct DebugInfo {
// Cached debug info, keyed by module path.
modules: HashMap<PathBuf, ModuleDebugInfo>,
// Set of module paths known to lack debug info.
no_debug_info: HashSet<PathBuf>,
}
impl DebugInfo {
/// Try to load debug info for a module.
///
/// If debug info was founded and loaded (now or previously), returns
/// `true`. If the module does not have debug info, returns `false`.
pub fn load_module(&mut self, module: PathBuf) -> Result<bool> {
if self.no_debug_info.contains(&module) {
return Ok(false);
}
if self.modules.get(&module).is_some() {
return Ok(true);
}
let info = match ModuleDebugInfo::load(&module)? {
Some(info) => info,
None => {
self.no_debug_info.insert(module);
return Ok(false);
}
};
self.modules.insert(module, info);
Ok(true)
}
/// Fetch debug info for `module`, if loaded.
///
/// Does not attempt to load debug info for the module.
pub fn get(&self, module: impl AsRef<Path>) -> Option<&ModuleDebugInfo> {
self.modules.get(module.as_ref())
}
}
/// Debug info for a single executable module.
pub struct ModuleDebugInfo {
/// Backing debug info file data for the module.
///
/// May not include the actual executable code.
pub object: Object<'static>,
/// Interface and caching structures for general debug info querying.
pub session: ObjectDebugSession<'static>,
/// Cache which allows efficient source line lookups.
pub source: SymCache<'static>,
}
impl ModuleDebugInfo {
/// Load debug info for a module.
///
/// Returns `None` when the module was found and loadable, but no matching
/// debug info could be found.
///
/// Leaks module and symbol data.
fn load(module: &Path) -> Result<Option<Self>> {
let mut data = fs::read(&module)?.into_boxed_slice();
// If our module is a PE file, the debug info will be in the PDB.
//
// We will need a similar check to support split DWARF.
let is_pe = pe::PeObject::test(&data);
if is_pe {
// Assume a sibling PDB.
//
// TODO: Find PDB using `dbghelp::`SymGetSymbolFile()`.
let pdb = module.with_extension("pdb");
data = fs::read(&pdb)?.into_boxed_slice();
}
// Now we're sure we want this data, so leak it.
let data = Box::leak(data);
let object = Object::parse(data)?;
if !object.has_debug_info() {
return Ok(None);
}
let session = object.debug_session()?;
let cursor = io::Cursor::new(vec![]);
let cursor = SymCacheWriter::write_object(&object, cursor)?;
let cache_data = Box::leak(cursor.into_inner().into_boxed_slice());
let source = SymCache::parse(cache_data)?;
Ok(Some(Self {
object,
session,
source,
}))
}
}

View File

@ -18,6 +18,7 @@ pub mod elf;
pub mod block;
pub mod cache;
pub mod code;
pub mod debuginfo;
pub mod demangle;
pub mod report;
pub mod sancov;

View File

@ -39,9 +39,10 @@ cargo fmt -- --check
# RUSTSEC-2020-0036: a dependency `failure` (pulled from proc-maps) is deprecated
# RUSTSEC-2019-0036: a dependency `failure` (pulled from proc-maps) has type confusion vulnerability
# RUSTSEC-2021-0065: a dependency `anymap` is no longer maintained
# RUSTSEC-2020-0077: `memmap` dependency unmaintained, via `symbolic` (see: `getsentry/symbolic#304`)
# RUSTSEC-2020-0159: potential segfault in `time`, not yet patched (#1366)
# RUSTSEC-2020-0071: potential segfault in `chrono`, not yet patched (#1366)
cargo audit --deny warnings --deny unmaintained --deny unsound --deny yanked --ignore RUSTSEC-2020-0016 --ignore RUSTSEC-2020-0036 --ignore RUSTSEC-2019-0036 --ignore RUSTSEC-2021-0065 --ignore RUSTSEC-2020-0159 --ignore RUSTSEC-2020-0071
cargo audit --deny warnings --deny unmaintained --deny unsound --deny yanked --ignore RUSTSEC-2020-0016 --ignore RUSTSEC-2020-0036 --ignore RUSTSEC-2019-0036 --ignore RUSTSEC-2021-0065 --ignore RUSTSEC-2020-0159 --ignore RUSTSEC-2020-0071 --ignore RUSTSEC-2020-0077
cargo-license -j > data/licenses.json
cargo build --release --locked
cargo clippy --release -- -D warnings