Spracherkennung für: .rs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, Result};
use regex::{RegexSet, RegexSetBuilder};
use serde_derive::{Deserialize, Serialize};
use toml;
use wast2js;
use log::{debug, info, warn};
// Data structures
#[derive(Debug, Default, Serialize, Deserialize)]
struct Config {
#[serde(default)]
harness_directive: Option<String>,
#[serde(default)]
directive: Option<String>,
#[serde(default)]
included_tests: Vec<String>,
#[serde(default)]
excluded_tests: Vec<String>,
repos: Vec<Repo>,
}
impl Config {
fn find_repo_mut(&mut self, name: &str) -> Option<&mut Repo> {
self.repos.iter_mut().find(|x| &x.name == name)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Repo {
name: String,
url: String,
#[serde(default)]
branch: Option<String>,
#[serde(default)]
parent: Option<String>,
#[serde(default)]
directive: Option<String>,
#[serde(default)]
included_tests: Vec<String>,
#[serde(default)]
excluded_tests: Vec<String>,
#[serde(default)]
skip_wast: bool,
#[serde(default)]
skip_js: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Lock {
repos: Vec<LockRepo>,
}
impl Lock {
fn find_commit(&self, name: &str) -> Option<&str> {
self.repos
.iter()
.find(|x| &x.name == name)
.map(|x| x.commit.as_ref())
}
fn set_commit(&mut self, name: &str, commit: &str) {
if let Some(lock) = self.repos.iter_mut().find(|x| &x.name == name) {
lock.commit = commit.to_owned();
} else {
self.repos.push(LockRepo {
name: name.to_owned(),
commit: commit.to_owned(),
});
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct LockRepo {
name: String,
commit: String,
}
#[derive(Debug)]
enum Merge {
Standalone,
Merged,
Conflicted,
}
#[derive(Debug)]
struct Status {
commit_base_hash: String,
commit_final_message: String,
merged: Merge,
built: bool,
}
// Roll-your-own CLI utilities
fn run(name: &str, args: &[&str]) -> Result<String> {
debug!("{} {:?}", name, args);
let output = Command::new(name).args(args).output()?;
let stdout = String::from_utf8(output.stdout)?.trim().to_owned();
let stderr = String::from_utf8(output.stderr)?.trim().to_owned();
if !stdout.is_empty() {
debug!("{}", stdout);
}
if !stderr.is_empty() {
debug!("{}", stderr);
}
if output.status.success() {
Ok(stdout)
} else {
bail!("{}: {}\n{}", name.to_owned(), stdout, stderr)
}
}
fn change_dir(dir: &str) -> impl Drop {
#[must_use]
struct Reset {
previous: PathBuf,
}
impl Drop for Reset {
fn drop(&mut self) {
debug!("cd {}", self.previous.display());
env::set_current_dir(&self.previous).unwrap()
}
}
let previous = Reset {
previous: env::current_dir().unwrap(),
};
debug!("cd {}", dir);
env::set_current_dir(dir).unwrap();
previous
}
fn find(dir: &str) -> Vec<PathBuf> {
let mut paths = Vec::new();
fn find(dir: &str, paths: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).unwrap().map(|x| x.unwrap()) {
let path = entry.path();
if entry.file_type().unwrap().is_dir() {
find(path.to_str().unwrap(), paths);
} else {
paths.push(path);
}
}
}
find(dir, &mut paths);
paths
}
fn write_string<P: AsRef<Path>>(path: P, text: &str) -> Result<()> {
let path = path.as_ref();
if let Some(dir) = path.parent() {
let _ = fs::create_dir_all(dir);
}
fs::write(path, text.as_bytes())?;
Ok(())
}
// The main script
fn main() {
env_logger::init();
// Load the config
let mut config: Config =
toml::from_str(&fs::read_to_string("config.toml").expect("failed to read config.toml"))
.expect("invalid config.toml");
// Load the lock file, or default to no pinned commits
let mut lock: Lock = if Path::new("config-lock.toml").exists() {
toml::from_str(
&fs::read_to_string("config-lock.toml").expect("failed to read config-lock.toml"),
)
.expect("invalid config-lock.toml")
} else {
Lock::default()
};
// Clean old tests and initialize the repo if it doesn't exist
let specs_dir = "specs/";
clean_and_init_dirs(specs_dir);
// Generate the tests
let mut successes = Vec::new();
let mut failures = Vec::new();
{
// Change to the `specs/` dir where all the work happens
let _cd = change_dir(specs_dir);
for repo in &config.repos {
info!("Processing {:#?}", repo);
match build_repo(repo, &config, &lock) {
Ok(status) => successes.push((repo.name.clone(), status)),
Err(err) => failures.push((repo.name.clone(), err)),
};
}
}
// Abort if we had a failure
if !failures.is_empty() {
warn!("Failed.");
for (name, err) in &failures {
warn!("{}: (failure) {:?}", name, err);
}
std::process::exit(1);
}
// Display successful results
info!("Done.");
for (name, status) in &successes {
let repo = config.find_repo_mut(&name).unwrap();
lock.set_commit(&name, &status.commit_base_hash);
info!(
"{}: ({} {}) {}",
repo.name,
match status.merged {
Merge::Standalone => "standalone",
Merge::Merged => "merged",
Merge::Conflicted => "conflicted",
},
if status.built { "building" } else { "broken" },
status.commit_final_message.trim_end()
);
}
// Commit the new lock file
write_string("config-lock.toml", &toml::to_string_pretty(&lock).unwrap()).unwrap();
}
fn clean_and_init_dirs(specs_dir: &str) {
if !Path::new(specs_dir).exists() {
fs::create_dir(specs_dir).unwrap();
run("git", &["-C", specs_dir, "init"]).unwrap();
}
let _ = fs::remove_dir_all("./tests");
}
fn build_repo(repo: &Repo, config: &Config, lock: &Lock) -> Result<Status> {
let remote_name = &repo.name;
let remote_url = &repo.url;
let remote_branch = repo.branch.as_ref().map(|x| x.as_str()).unwrap_or("master");
let branch_upstream = format!("{}/{}", repo.name, remote_branch);
let branch_base = repo.name.clone();
// Initialize our remote and branches if they don't exist
let remotes = run("git", &["remote"])?;
if !remotes.lines().any(|x| x == repo.name) {
run("git", &["remote", "add", remote_name, &remote_url])?;
run("git", &["fetch", remote_name])?;
run("git", &["branch", &branch_base, &branch_upstream])?;
}
// Set the upstream to the correct branch
run(
"git",
&[
"branch",
&branch_base,
"--set-upstream-to",
&branch_upstream,
],
)?;
// Fetch the latest changes for this repo
run("git", &["fetch", remote_name])?;
// Checkout the pinned commit, if any, and get the absolute commit hash
let base_treeish = lock.find_commit(&repo.name).unwrap_or(&branch_upstream);
run("git", &["checkout", &branch_base])?;
run("git", &["reset", base_treeish, "--hard"])?;
let commit_base_hash = run("git", &["log", "--pretty=%h", "-n", "1"])?
.trim()
.to_owned();
// Try to merge with parent repo, if specified
let merged = try_merge_parent(repo, &commit_base_hash)?;
// Exclude files specified from the config and repo
let mut excluded_files = Vec::new();
excluded_files.extend_from_slice(&config.excluded_tests);
excluded_files.extend_from_slice(&repo.excluded_tests);
let exclude = RegexSetBuilder::new(&excluded_files).build().unwrap();
// Try to build the test suite on this commit. This may fail due to merging
// with a parent repo, in which case we will try again in an unmerged state.
let mut built = false;
match try_build_tests(&exclude) {
Ok(()) => built = true,
Err(err) => warn!("Failed to build tests: {:?}", err),
};
// if try_build_tests(&exclude).is_err() {
// if repo.parent.is_some() {
// warn!(
// "Failed to build interpreter. Retrying on unmerged commit ({})",
// &commit_base_hash
// );
// run("git", &["reset", &commit_base_hash, "--hard"])?;
// built = try_build_tests(&exclude).is_ok();
// } else {
// built = false;
// }
// }
// if !built {
// warn!("Failed to build interpreter, Won't emit js/html");
// }
// Get the final commit message we ended up on
let commit_final_message = run("git", &["log", "--oneline", "-n", "1"])?;
// Compute the source files that changed, and use that to filter the files
// we copy over. We can't compare the generated tests, because for a
// generated WPT we need to copy both the .js and .html even if only
// one of those is different from the master.
let tests_changed = find_tests_changed(repo)?;
info!("Changed tests: {:#?}", tests_changed);
// Include the changed tests, specified files, and `harness/` directory
let mut included_files = Vec::new();
included_files.extend_from_slice(&tests_changed);
included_files.extend_from_slice(&config.included_tests);
included_files.extend_from_slice(&repo.included_tests);
included_files.push("harness/".to_owned());
let include = RegexSetBuilder::new(&included_files).build().unwrap();
// Copy over all the desired test-suites
if !repo.skip_wast {
copy_tests(repo, "test/core", "../tests", "wast", &include, &exclude);
}
if built && !repo.skip_js {
copy_tests(repo, "js", "../tests", "js", &include, &exclude);
copy_directives(repo, config)?;
}
Ok(Status {
commit_final_message,
commit_base_hash,
merged,
built,
})
}
fn try_merge_parent(repo: &Repo, commit_base_hash: &str) -> Result<Merge> {
if !repo.parent.is_some() {
return Ok(Merge::Standalone);
}
let parent = repo.parent.as_ref().unwrap();
// Try to merge with the parent branch.
let message = format!("Merging {}:{}with {}", repo.name, commit_base_hash, parent);
Ok(
if !run("git", &["merge", "-q", parent, "-m", &message]).is_ok() {
// Ignore merge conflicts in the document directory.
if !run("git", &["checkout", "--ours", "document"]).is_ok()
|| !run("git", &["add", "document"]).is_ok()
|| !run("git", &["-c", "core.editor=true", "merge", "--continue"]).is_ok()
{
// Reset to master if we failed
warn!(
"Failed to merge {}, falling back to {}.",
repo.name, &commit_base_hash
);
run("git", &["merge", "--abort"])?;
run("git", &["reset", &commit_base_hash, "--hard"])?;
Merge::Conflicted
} else {
Merge::Merged
}
} else {
Merge::Merged
},
)
}
fn try_build_tests(exclude: &RegexSet) -> Result<()> {
let _ = fs::remove_dir_all("./js");
fs::create_dir("./js")?;
let paths = find("./test/core/");
for path in paths {
if path.extension() != Some(OsStr::new("wast")) {
continue;
}
let stripped_path = path.strip_prefix("./test/core/").unwrap();
let stripped_path_str = stripped_path.to_str().unwrap();
if exclude.is_match(stripped_path_str) {
continue;
}
let source = std::fs::read_to_string(&path)?;
let script = wast2js::convert(&path, &source)?;
std::fs::write(
Path::new("./js").join(&path.with_extension("wast.js").file_name().unwrap()),
&script,
)?;
}
fs::create_dir("./js/harness")?;
write_string("./js/harness/harness.js", &wast2js::harness())?;
Ok(())
}
fn copy_tests(
repo: &Repo,
src_dir: &str,
dst_dir: &str,
test_name: &str,
include: &RegexSet,
exclude: &RegexSet,
) {
for path in find(src_dir) {
let stripped_path = path.strip_prefix(src_dir).unwrap();
let stripped_path_str = stripped_path.to_str().unwrap();
if !include.is_match(stripped_path_str) || exclude.is_match(stripped_path_str) {
continue;
}
let out_path = Path::new(dst_dir)
.join(test_name)
.join(&repo.name)
.join(&stripped_path);
let out_dir = out_path.parent().unwrap();
let _ = fs::create_dir_all(out_dir);
fs::copy(path, out_path).unwrap();
}
}
fn copy_directives(repo: &Repo, config: &Config) -> Result<()> {
// Write directives files
if let Some(harness_directive) = &config.harness_directive {
let directives_path = Path::new("../tests/js")
.join(&repo.name)
.join("harness/directives.txt");
write_string(&directives_path, harness_directive)?;
}
let directives = format!(
"{}{}",
config.directive.as_ref().map(|x| x.as_str()).unwrap_or(""),
repo.directive.as_ref().map(|x| x.as_str()).unwrap_or("")
);
if !directives.is_empty() {
let directives_path = Path::new("../tests/js")
.join(&repo.name)
.join("directives.txt");
write_string(&directives_path, &directives)?;
}
Ok(())
}
fn find_tests_changed(repo: &Repo) -> Result<Vec<String>> {
let files_changed = if let Some(parent) = repo.parent.as_ref() {
run(
"git",
&["diff", "--name-only", &repo.name, &parent, "test/core"],
)?
.lines()
.map(|x| PathBuf::from(x))
.collect()
} else {
find("test/core")
};
let mut tests_changed = Vec::new();
for path in files_changed {
if path.extension().map(|x| x.to_str().unwrap()) != Some("wast") {
continue;
}
let name = path.file_name().unwrap().to_str().unwrap().to_owned();
tests_changed.push(name);
}
Ok(tests_changed)
}