diff options
author | Joseph Hunkeler <jhunkeler@gmail.com> | 2019-05-25 01:48:00 -0400 |
---|---|---|
committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2019-05-25 01:48:00 -0400 |
commit | 2ac8f0c609bf52dce1ea2ec6d42182ac9ce982fb (patch) | |
tree | cff669945f0711d1d53f5621e54b9186b1b0c174 | |
parent | 41357ebccf9f6a73b8f11c9d335a0f970e793b32 (diff) | |
download | dm-2ac8f0c609bf52dce1ea2ec6d42182ac9ce982fb.tar.gz |
Basic integration testing; uses struct instead of asssociativeArray; ensure easyinstall is usable by pip/setup.py
-rw-r--r-- | source/app.d | 19 | ||||
-rw-r--r-- | source/conda.d | 69 | ||||
-rw-r--r-- | source/merge.d | 80 | ||||
-rw-r--r-- | source/util.d | 96 |
4 files changed, 209 insertions, 55 deletions
diff --git a/source/app.d b/source/app.d index eb7938f..71b8d9e 100644 --- a/source/app.d +++ b/source/app.d @@ -15,6 +15,9 @@ int main(string[] args) { string installer_variant = "3"; string installer_version = "4.5.12"; bool run_tests = false; + string test_program = "pytest"; + string test_args = "-v"; // arguments to pass to test runner + string test_requires; // pip requirements file string mergefile; string base_spec; @@ -33,6 +36,9 @@ int main(string[] args) { "install-variant", "miniconda Python variant", &installer_variant, "install-version|i", "version of miniconda installer", &installer_version, "run-tests|R", "scan merged packages and execute their tests", &run_tests, + "test-program", "program that will execute tests", &test_program, + "test-args", "arguments passed to test executor", &test_args, + "test-requires", "path to pip requirements file", &test_requires, "base-spec", "conda explicit or yaml environment dump file", &base_spec ); @@ -50,6 +56,10 @@ int main(string[] args) { output_dir = buildPath(output_dir, env_name).absolutePath; mergefile = buildPath(mergefile).absolutePath; + if (!test_requires.empty) { + test_requires = buildPath(test_requires).absolutePath; + } + if (installer_variant != "3") { writeln("Python 2.7 has reached end-of-life."); writeln("3.x variant will be used instead."); @@ -103,6 +113,15 @@ int main(string[] args) { conda.dump_env_yaml(buildPath(output_dir, env_name ~ ".yml")); conda.dump_env_explicit(buildPath(output_dir, env_name ~ ".txt")); + if (run_tests) { + string testdir = buildPath(output_dir, "testdir"); + test_runner_t runner = test_runner_t(test_program, test_args, test_requires); + testable_t[] testable = testable_packages(conda, mergefile); + foreach (t; testable) { + integration_test(conda, testdir, runner, t); + } + } + writeln("Done!"); return 0; } diff --git a/source/conda.d b/source/conda.d index 2f447bd..ff44b50 100644 --- a/source/conda.d +++ b/source/conda.d @@ -8,48 +8,7 @@ import std.system; import std.path; import std.process; import std.typecons; - - -static auto getenv(string[string] base=null, string preface=null) { - const char delim = '='; - char delim_line = '\n'; - string[string] env; - string cmd = "env"; - - version (linux) { - cmd ~= " -0"; - delim_line = '\0'; - } - - version (Windows) { - cmd = "set"; - delim_line = "\r\n"; - } - - // Execute a command before dumping the environment - if (preface !is null) { - cmd = preface ~ " && " ~ cmd; - } - - auto env_sh = executeShell(cmd, env=base); - if (env_sh.status) { - throw new Exception("Unable to read shell environment:" ~ env_sh.output); - } - - foreach (string line; split(env_sh.output, delim_line)) { - if (line.empty) { - continue; - } - auto data = split(line, delim); - - // Recombine extra '=' chars - if (data.length > 2) { - data[1] = join(data[1 .. $], delim); - } - env[data[0]] = data[1]; - } - return env; -} +import util; class Conda { @@ -216,6 +175,7 @@ class Conda { this.env["PATH"]], pathSeparator); this.configure_headless(); + this.fix_setuptools(); this.initialized = true; } @@ -223,6 +183,7 @@ class Conda { this.env_orig = this.env.dup; string[string] env_new = getenv(this.env, "source activate " ~ name); this.env = env_new.dup; + this.fix_setuptools(); } void deactivate() { @@ -240,7 +201,7 @@ class Conda { } int sh(string command) { - writeln("Running: " ~ command); + banner('#', command); auto proc = spawnShell(command, env=this.env); scope(exit) wait(proc); return wait(proc); @@ -275,6 +236,28 @@ class Conda { return buildPath(this.install_prefix, "envs", name).exists; } + string env_where(string name) { + if (this.env_exists(name)) { + return buildPath(this.install_prefix, "envs", name); + } + return null; + } + + string env_current() { + return this.env.get("CONDA_PREFIX", null); + } + + string site() { + return this.sh_block("python -c 'import site; print(site.getsitepackages()[0])'").output.strip; + } + + void fix_setuptools() { + string pthfile = buildPath(this.site(), "easy-install.pth"); + if (!pthfile.exists) { + File(pthfile, "w+").write(""); + } + } + string dump_env_yaml(string filename=null) { string args; if (filename !is null) { diff --git a/source/merge.d b/source/merge.d index af49fa7..f63a5da 100644 --- a/source/merge.d +++ b/source/merge.d @@ -11,6 +11,7 @@ import std.regex; import std.stdio; import std.string; import conda; +import util; import dyaml : dumper, Loader, Node; @@ -20,17 +21,15 @@ auto RE_DMFILE_INVALID_VERSION = regex(r"[ !@#$%^&\*\(\)\-_]+"); auto RE_DELIVERY_NAME = regex(r"(?P<name>.*)[-_](?P<version>.*)[-_]py(?P<python_version>\d+)[-_.](?P<iteration>\d+)[-_.](?P<ext>.*)"); -string safe_spec(string s) { - return "'" ~ s ~ "'"; +struct test_runner_t { + string program; + string args; + string requires; } - -string safe_install(string[] specs) { - string[] result; - foreach (record; specs) { - result ~= safe_spec(record); - } - return result.join(" "); +struct testable_t { + string repo; + string head; } @@ -87,8 +86,8 @@ bool env_combine(ref Conda conda, string name, string specfile, string mergefile } -string[string][] testable_packages(ref Conda conda, string mergefile) { - string[string][] results; +testable_t[] testable_packages(ref Conda conda, string mergefile) { + testable_t[] results; foreach (record; dmfile(mergefile)) { Node meta; string pkg_d; @@ -139,7 +138,64 @@ string[string][] testable_packages(ref Conda conda, string mergefile) { repository = ""; } - results ~= ["repo": repository, "commit": head]; + results ~= testable_t(repository, head); } return results; } + +auto integration_test(ref Conda conda, string outdir, test_runner_t runner, testable_t pkg) { + import core.stdc.stdlib : exit; + import std.ascii : letters; + import std.conv : to; + import std.random : randomSample; + import std.utf : byCodeUnit; + + string cwd = getcwd().absolutePath; + scope (exit) cwd.chdir; + string repo_root = buildPath(outdir, pkg.repo.baseName) + .replace(".git", ""); + outdir.mkdirRecurse; + + if (!repo_root.exists) { + if (conda.sh("git clone --recursive " ~ pkg.repo ~ " " ~ repo_root)) { + exit(1); + } + } + + repo_root.chdir; + + if (conda.sh("git checkout " ~ pkg.head)) { + exit(1); + } + + foreach (string found; conda.scan_packages(repo_root.baseName ~ "*")) { + string[] tmp = found.split("-"); + found = tmp[0]; + // Does not need to succeed for all matches + conda.run("uninstall " ~ found); + } + + if (runner.requires) { + if (conda.sh("python -m pip install -r " ~ runner.requires)) { + exit(1); + } + } + + if (conda.sh("python -m pip install .[test]")) { + exit(1); + } + + if (conda.sh("python setup.py egg_info")) { + exit(1); + } + + auto id = letters.byCodeUnit.randomSample(6).to!string; + string basetemp = tempDir.buildPath("dm_testable" ~ id); + basetemp.mkdir; + scope(exit) basetemp.rmdirRecurse; + + if (conda.sh(runner.program ~ " " ~ runner.args ~ " --basetemp=" ~ basetemp) > 1) { + exit(1); + } + return 0; +} diff --git a/source/util.d b/source/util.d new file mode 100644 index 0000000..d49f9d7 --- /dev/null +++ b/source/util.d @@ -0,0 +1,96 @@ +module util; +import std.stdio; +import std.string; +import std.process; + + +enum byte MAXCOLS = 80; + + +void banner(const char ch, string s) { + string ruler; + byte i = 0; + while (i < MAXCOLS) { + ruler ~= ch; + i++; + } + + string result; + string[] tmpstr = splitLines(wrap(s, MAXCOLS - 2, ch ~ " ", ch ~ " ")); + foreach (idx, line; tmpstr) { + if (idx < tmpstr.length - 1) { + line ~= " \\"; + } + result ~= line ~ "\n"; + } + + writeln(ruler); + write(result); + writeln(ruler); +} + + +static auto getenv(string[string] base=null, string preface=null) { + const char delim = '='; + char delim_line = '\n'; + string[string] env; + string cmd = "env"; + + version (linux) { + cmd ~= " -0"; + delim_line = '\0'; + } + + version (Windows) { + cmd = "set"; + delim_line = "\r\n"; + } + + // Execute a command before dumping the environment + if (preface !is null) { + cmd = preface ~ " && " ~ cmd; + } + + auto env_sh = executeShell(cmd, env=base); + if (env_sh.status) { + throw new Exception("Unable to read shell environment:" ~ env_sh.output); + } + + foreach (string line; split(env_sh.output, delim_line)) { + if (line.empty) { + continue; + } + auto data = split(line, delim); + + // Recombine extra '=' chars + if (data.length > 2) { + data[1] = join(data[1 .. $], delim); + } + env[data[0]] = data[1]; + } + return env; +} + + +string safe_spec(string s) { + return "'" ~ s ~ "'"; +} + + +string safe_install(string[] specs) { + string[] result; + foreach (record; specs) { + result ~= safe_spec(record); + } + return result.join(" "); +} + + +string safe_install(string specs) { + string[] result; + foreach (record; specs.split(" ")) { + result ~= safe_spec(record); + } + return result.join(" "); +} + |