Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  main.rs   Sprache: unbekannt

 
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)
}

[ Dauer der Verarbeitung: 0.27 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge