diff --git a/docker/rust.docker b/docker/rust.docker index 2ef75840..48b7b40e 100644 --- a/docker/rust.docker +++ b/docker/rust.docker @@ -1,14 +1,20 @@ FROM codewars/base-runner -# Setup env +RUN ln -s /home/codewarrior /workspace + +COPY frameworks/rust/skeleton /workspace/rust +RUN chown -R codewarrior:codewarrior /workspace/rust + USER codewarrior ENV USER=codewarrior HOME=/home/codewarrior # Install rustup with the Rust v1.15.1 toolchain RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.15.1 +# ~/.cargo/env +ENV PATH $HOME/.cargo/bin:$PATH +RUN cd /workspace/rust && cargo build && rm src/lib.rs USER root -RUN ln -s /home/codewarrior /workspace ENV NPM_CONFIG_LOGLEVEL warn WORKDIR /runner @@ -26,8 +32,8 @@ COPY test/runners/rust_spec.js test/runners/ USER codewarrior ENV USER=codewarrior HOME=/home/codewarrior -# ~/.cargo/env ENV PATH=$HOME/.cargo/bin:$PATH + RUN mocha -t 10000 test/runners/rust_spec.js ENTRYPOINT ["node"] diff --git a/examples/rust.yml b/examples/rust.yml index 1d893d94..a785fdaa 100644 --- a/examples/rust.yml +++ b/examples/rust.yml @@ -1,4 +1,4 @@ -test: +rust: algorithms: initial: |- // Return the two oldest/oldest ages within the vector of ages passed in. @@ -69,3 +69,10 @@ test: format!("Hello {}, my name is {}", your_name, self.name) } } + + fixture: |- + #[test] + fn greet_is_correct() { + let p = Person { name: "Bill" }; + assert_eq!(p.greet("Jack"), "Hello Jack, my name is Bill"); + } diff --git a/frameworks/rust/skeleton/Cargo.toml b/frameworks/rust/skeleton/Cargo.toml new file mode 100644 index 00000000..30757383 --- /dev/null +++ b/frameworks/rust/skeleton/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "codewars" +version = "0.0.1" +authors = ["codewarrior"] + +[dependencies] +rand = "0.3" diff --git a/frameworks/rust/skeleton/src/lib.rs b/frameworks/rust/skeleton/src/lib.rs new file mode 100644 index 00000000..86a903fc --- /dev/null +++ b/frameworks/rust/skeleton/src/lib.rs @@ -0,0 +1,9 @@ +pub fn add(x: i32, y: i32) -> i32 { x + y } + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(add(1, 1), 2); + } +} diff --git a/lib/runners/rust.js b/lib/runners/rust.js index 0aa63673..94e962aa 100644 --- a/lib/runners/rust.js +++ b/lib/runners/rust.js @@ -1,91 +1,121 @@ 'use strict'; const shovel = require('../shovel'); -const util = require('../util'); -const exec = require('child_process').exec; - -let usingTests = false; +const fs = require('fs'); module.exports.run = function run(opts, cb) { shovel.start(opts, cb, { solutionOnly: function(runCode, fail) { - usingTests = false; - - const code = `${opts.setup ? opts.setup+'\n' : ''}${opts.code}`; - util.codeWriteSync('rust', code, opts.dir, 'main.rs', true); - - _compile((err,stdout,stderr) => { - if (err) return fail({stdout, stderr}); - - // Run - runCode({ - name: `./main`, - options: { - cwd: opts.dir - } - }); + fs.writeFileSync('/workspace/rust/src/main.rs', + `${opts.setup ? opts.setup+'\n' : ''}${opts.solution}`); + runCode({ + name: 'cargo', + args: ['run'], + options: { + cwd: '/workspace/rust', + env: Object.assign({}, process.env, { + RUSTFLAGS: rustFlags(), + }), + }, }); }, - testIntegration: function(runCode, fail) { - usingTests = true; + testIntegration: function(runCode, fail) { if (opts.testFramework != 'rust') - throw 'Test framework is not supported'; - - const code = `${opts.setup ? opts.setup+'\n' : ''}${opts.code}\n${opts.fixture}`; - util.codeWriteSync('rust', code, opts.dir, 'main.rs', true); - - _compile((err,stdout,stderr) => { - if (err) return fail({stdout, stderr}); - - // Run - runCode({ - name: `./main`, - options: { - cwd: opts.dir - } - }); + throw new Error(`Test framework '${opts.testFramework}' is not supported`); + + // + // needs some hack in order to maintain backward compatibility and avoid errors with duplicate imports + // . + // |- Cargo.toml + // `- src + // |- lib.rs (opt.setup + opts.solution + module declaration) + // `- tests.rs (opts.fixture) + // - placing `opts.fixture` in submodule `tests.rs` should avoid issues with duplicate imports + // - maintains backward compatibility by prepending `use super::*;` to `opts.fixture` if missing + // + fs.writeFileSync('/workspace/rust/src/lib.rs', + `${opts.setup ? opts.setup+'\n' : ''}${opts.solution}\n#[cfg(test)]\nmod tests;`); + var fixture = opts.fixture; + if (!fixture.includes('use super::*;')) { // HACK backward compatibility + fixture = 'use super::*;\n' + fixture; + } + fs.writeFileSync('/workspace/rust/src/tests.rs', fixture); + runCode({ + name: 'cargo', + args: ['test'], + options: { + cwd: '/workspace/rust', + env: Object.assign({}, process.env, { + RUSTFLAGS: rustFlags(), + }), + }, }); }, - sanitizeStdOut: function(stdout) { - if (opts.fixture) return _formatOut(stdout); - return stdout; - }, + transformBuffer: function(buffer) { - if (opts.testFramework == 'rust') { - // if tests failed then just output the entire raw test spec results so that the full details can be viewed - if (!buffer.stderr && buffer.stdout.indexOf('FAILED') > 0 && buffer.stdout.indexOf('failures:') > 0) { - buffer.stderr = buffer.stdout.substr(buffer.stdout.indexOf('failures:') + 10).replace("note: Run with `RUST_BACKTRACE=1` for a backtrace.", ''); - // trim after the first failures section - buffer.stderr = "Failure Info:\n" + buffer.stderr.substr(0, buffer.stderr.indexOf('failures:')); - if (opts.setup) { - buffer.stderr += "\nNOTE: Line numbers reported within errors will not match up exactly to those shown within your editors due to concatenation."; - } - } + if (!opts.fixture) return; + + const ss = buffer.stdout.split(/\n((?:---- \S+ stdout ----)|failures:)\n/); + // Save test failures in object to use in output. + // There shouldn't be name collisions since test cases are rust functions. + const failures = {}; + for (let i = 1; i < ss.length; ++i) { + const s = ss[i]; + const m = s.match(/^---- (\S+) stdout ----$/); + if (m === null) continue; + const name = m[1]; + const fail = []; + const x = ss[++i]; + const m2 = x.match(/thread '[^']+' panicked at '(.+)'.*/); + if (m2.index != 1) fail.push(x.slice(1, m2.index)); // user logged output + fail.push(`Test Failed<:LF:>${m2[1].replace(/\\'/g, "'")}`); + failures[name] = fail; } - } - }); - - // Initalizes the Rust dir via cargo - const _compile = function(cb) { - exec(`rustc main.rs ${usingTests ? '--test' : ''}`,{cwd: opts.dir}, cb); - }; - - const _formatOut = function(stdout) { - let output = ''; - let tests = stdout.split(/\n/gm).filter(v => !v.search(/^test.*(?:ok|FAILED)$/)); - for (let test of tests) output += _parseTest(test); - - return output; - }; + const out = []; + for (const s of ss[0].split('\n')) { + const m = s.match(/^test (\S+) \.{3} (FAILED|ok)$/); + if (m === null) continue; + out.push(`${m[1].replace(/^tests::/, '')}`); + if (m[2] == 'ok') { + out.push('Test Passed'); + } + else { + out.push.apply(out, failures[m[1]]); + } + out.push(``); + } + out.push(''); + buffer.stdout = out.join('\n'); + }, - const _parseTest = function(test) { - let result = test.split(' '); - let out = `${result[1]}\n`; - out += result[3] != 'FAILED' ? `Test Passed\n` : `Test Failed\n`; - return out; - }; + sanitizeStdErr: function(stderr) { + // remove logs from cargo test + stderr = stderr + .replace(/^\s+Compiling .*$/gm, '') + .replace(/^\s+Finished .*$/m, '') + .replace(/^\s+Running .*$/m, '') + .replace(/^\s+Doc-tests .*$/m, '') + .replace('To learn more, run the command again with --verbose.', '') + .replace(/^error: test failed$/m, '') + .replace(/^error: Could not compile .*$/m, '') + .trim(); + if (stderr !== '') stderr += '\n'; + if (/^error: /m.test(stderr) && opts.setup) { + stderr += "\nNOTE: Line numbers reported within errors will not match up exactly to those shown within your editors due to concatenation.\n"; + } + return stderr; + }, + }); }; - +// ignore some warnings for backward compatibility +function rustFlags() { + const flags = []; + if (process.env.RUSTFLAGS !== '') flags.push(process.env.RUSTFLAGS); + flags.push('-A', 'dead_code'); + flags.push('-A', 'unused_imports'); + flags.push('-A', 'unused_variables'); + return flags.join(' '); +} diff --git a/test/runners/rust_spec.js b/test/runners/rust_spec.js index a477f2f9..4eb3fc78 100644 --- a/test/runners/rust_spec.js +++ b/test/runners/rust_spec.js @@ -1,32 +1,28 @@ +"use strict"; + const expect = require('chai').expect; const runner = require('../runner'); +const exec = require('child_process').exec; describe('rust runner', function() { - describe('.run', function() { - it('should handle basic code evaluation', function(done) { - runner.run({ - language: 'rust', - code: ` - fn main() { - println!("Bam"); - } - ` - }, function(buffer) { - expect(buffer.stdout).to.equal('Bam\n'); + describe('running', function() { + afterEach(function cleanup(done) { + exec('rm -f /workspace/rust/src/*.rs', function(err) { + if (err) return done(err); done(); }); }); - it('should handle invalid code', function(done) { + it('should handle basic code evaluation', function(done) { runner.run({ language: 'rust', code: ` fn main() { - println!("Bam); + println!("Bam"); } ` }, function(buffer) { - expect(buffer.stderr).to.contain('unterminated double quote'); + expect(buffer.stdout).to.equal('Bam\n'); done(); }); }); @@ -44,7 +40,7 @@ describe('rust runner', function() { thread::spawn(move|| { tx.send(10).unwrap(); }); - + println!("{}", rx.recv().unwrap()); } ` @@ -53,6 +49,19 @@ describe('rust runner', function() { done(); }); }); + }); + + describe('examples', function() { + runner.assertCodeExamples('rust'); + }); + + describe('testing', function() { + afterEach(function cleanup(done) { + exec('rm -f /workspace/rust/src/*.rs', function(err) { + if (err) return done(err); + done(); + }); + }); it('should handle basic tests', function(done) { runner.run({ @@ -70,8 +79,9 @@ describe('rust runner', function() { `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stdout).to.contain(`returns_number`); + expect(buffer.stdout).to.contain(`returns_number`); expect(buffer.stdout).to.contain(`Test Passed`); + expect(buffer.stderr).to.be.empty; done(); }); }); @@ -90,7 +100,7 @@ describe('rust runner', function() { tx.send(10).unwrap(); }); } - + fn doubler(n: i32) -> i32 { n * 2 } @@ -103,13 +113,14 @@ describe('rust runner', function() { `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stdout).to.contain(`doubler_works`); + expect(buffer.stdout).to.contain(`doubler_works`); expect(buffer.stdout).to.contain(`Test Passed`); + expect(buffer.stderr).to.be.empty; done(); }); }); - it('should handle broken and unused code', function(done) { + it('should handle failed tests', function(done) { runner.run({ language: 'rust', setup: ` @@ -117,151 +128,337 @@ describe('rust runner', function() { use std::sync::mpsc::channel; `, code: ` - fn async_thingo() { - let (tx, rx) = channel(); - thread::spawn(move|| { - tx.send(10).unwrap(); - }); - } - - fn unused_func() { - println!("Never called"); - } - - fn broken_func() { - println!("This is broken..."; - } - fn doubler(n: i32) -> i32 { n * 2 } `, fixture: ` - #[test] - fn doubler_works() { - assert_eq!(doubler(2),4); - } + #[test] + fn doubler_failure() { + assert_eq!(doubler(2),3); + } `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stderr).to.contain(`incorrect close delimiter`); + expect(buffer.stdout).to.contain(`doubler_failure`); + expect(buffer.stdout).to.contain(`Test Failed`); done(); }); }); - it('should ignore unused code warnings', function(done) { + it('should handle success and failed tests', function(done) { runner.run({ language: 'rust', - setup: ` - use std::thread; - use std::sync::mpsc::channel; - `, code: ` - fn async_thingo() { - let (tx, rx) = channel(); - thread::spawn(move|| { - tx.send(10).unwrap(); - }); - } - - fn unused_func() { - println!("Never called"); - } - fn doubler(n: i32) -> i32 { n * 2 } `, fixture: ` - #[test] - fn doubler_works() { - assert_eq!(doubler(2),4); - } - `, + #[test] + fn doubler_success() { + assert_eq!(doubler(2),4); + } + + #[test] + fn doubler_failure() { + assert_eq!(doubler(2),3); + } + `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stdout).to.contain(`doubler_works`); + expect(buffer.stdout).to.contain(`doubler_success`); expect(buffer.stdout).to.contain(`Test Passed`); + expect(buffer.stdout).to.contain(`doubler_failure`); + expect(buffer.stdout).to.contain(`Test Failed`); done(); }); }); - it('should handle failed tests', function(done) { + it('can use rand', function(done) { runner.run({ language: 'rust', - setup: ` - use std::thread; - use std::sync::mpsc::channel; + code: 'fn add(x: i32, y: i32) -> i32 { x + y }', + // need `self::` because `extern crate` loads to current namespace and tests are in `mod tests`. + fixture: ` + extern crate rand; + use self::rand::Rng; + use self::rand::distributions::{IndependentSample, Range}; + + #[test] + fn returns_sum() { + let between = Range::new(1, 10); + let mut rng = rand::thread_rng(); + let x = between.ind_sample(&mut rng); + let y = between.ind_sample(&mut rng); + assert_eq!(add(x, y), x + y); + } `, + testFramework: 'rust' + }, function(buffer) { + expect(buffer.stdout).to.contain(`returns_sum`); + expect(buffer.stdout).to.contain(`Test Passed`); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + it('can use rand, works even if the solution imports it', function(done) { + runner.run({ + language: 'rust', code: ` - fn doubler(n: i32) -> i32 { - n * 2 + extern crate rand; + use rand::Rng; + fn add(x: i32, y: i32) -> i32 { + let mut rng = rand::thread_rng(); + if rng.gen() { + rng.gen::() + } else { + x + y + } } `, fixture: ` + extern crate rand; + use self::rand::Rng; + use self::rand::distributions::{IndependentSample, Range}; + #[test] - fn doubler_failure() { - assert_eq!(doubler(2),3); + fn returns_sum() { + let between = Range::new(1, 10); + let mut rng = rand::thread_rng(); + let x = between.ind_sample(&mut rng); + let y = between.ind_sample(&mut rng); + println!("x: {}, y: {}", x, y); + assert_eq!(add(x, y), x + y); } - `, + `, + testFramework: 'rust' + }, function(buffer) { + expect(buffer.stderr).not.to.contain('an extern crate named `rand` has already been imported in this module'); + done(); + }); + }); + + it('should include custom messages on failure', function(done) { + runner.run({ + language: 'rust', + code: ` + fn add(x:i32, y:i32) -> i32 { + if x == 1 { + x + y + } else { + x + y + 1 + } + } + `, + fixture: ` + use super::*; + #[test] fn returns_sum2_1() { assert_eq!(add(2, 1), 3, "testing addition with {} and {}", 2, 1); } + `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stdout).to.contain(`doubler_failure`); + expect(buffer.stdout).to.contain(`returns_sum2_1`); expect(buffer.stdout).to.contain(`Test Failed`); + expect(buffer.stdout).to.contain(`testing addition with 2 and 1`); done(); }); }); - it('should handle broken test code', function(done) { + it('should show users logs for failed tests', function(done) { runner.run({ language: 'rust', code: ` - fn doubler(n: i32) -> i32 { - n * 2 + fn add(x:i32, y:i32) -> i32 { + if x == 1 { + println!("[passing test] x: {}, y: {}", x, y); + x + y + } else { + println!("[failing test] x: {}, y: {}", x, y); + x + y + 1 + } } `, fixture: ` - #[test] - fn doubler_works( { - assert_eq!(doubler(2),4); + use super::*; + #[test] fn returns_sum1_1() { assert_eq!(add(1, 1), 2); } + #[test] fn returns_sum2_1() { assert_eq!(add(2, 1), 3); } + `, + testFramework: 'rust' + }, function(buffer) { + expect(buffer.stdout).to.contain(`returns_sum1_1`); + expect(buffer.stdout).to.contain(`Test Passed`); + expect(buffer.stdout).not.to.contain(`[passing test] x: 1, y: 1`); + expect(buffer.stdout).to.contain(`[failing test] x: 2, y: 1`); + done(); + }); + }); + + it('should output correctly for some edge cases', function(done) { + runner.run({ + language: 'rust', + code: 'fn add(x:i32, y:i32) -> i32 { x + y }', + fixture: ` + use super::*; + #[test] fn returns_sum1_1() { assert_eq!(add(1, 1), 2); } + #[test] fn single_quotes() { assert_eq!("abc'", "'cba"); } + #[test] fn back_slashes() { assert_eq!("abc\\\\'", "'\\\\cba"); } + #[test] fn line_feed_issue() { + print!("foo"); + assert_eq!(1, 2); } `, testFramework: 'rust' }, function(buffer) { - console.log(buffer); - expect(buffer.stderr).to.contain(`un-closed delimiter`); + expect(buffer.stdout).to.contain(`returns_sum1_1`); + expect(buffer.stdout).to.contain(`\nTest Passed`); + expect(buffer.stdout).to.contain(`\nTest Failed<:LF:>assertion failed: \`(left == right)\` (left: \`"abc\\\\'"\`, right: \`"'\\\\cba"\`)`); + expect(buffer.stdout).to.contain(`\nTest Failed<:LF:>assertion failed: \`(left == right)\` (left: \`"abc'"\`, right: \`"'cba"\`)`); + expect(buffer.stdout).to.contain(`\nTest Failed<:LF:>assertion failed: \`(left == right)\` (left: \`1\`, right: \`2\`)`); done(); }); }); - it('should handle success and failed tests', function(done) { + it('should output correct structure', function(done) { + runner.run({ + language: 'rust', + code: 'fn add(x:i32, y:i32) -> i32 { x + y }', + fixture: ` + use super::*; + #[test] fn returns_sum1_1() { assert_eq!(add(1, 1), 2); } + #[test] fn returns_sum1_2() { assert_eq!(add(1, 2), 3); } + #[test] fn returns_sum1_3() { assert_eq!(add(1, 3), 4); } + #[test] fn returns_sum1_4() { assert_eq!(add(1, 4), 5); } + `, + testFramework: 'rust' + }, function(buffer) { + const expected = [ + '', + '', + '', + '' + ].join('').replace(/\s/g, ''); + expect(buffer.stdout.match(/<(?:IT|PASSED|FAILED|COMPLETEDIN)::>/g).join('')).to.equal(expected); + done(); + }); + }); + }); + + describe('invalid code and warnings', function() { + afterEach(function cleanup(done) { + exec('rm -f /workspace/rust/src/*.rs', function(err) { + if (err) return done(err); + done(); + }); + }); + + it('should handle invalid code', function(done) { runner.run({ language: 'rust', + code: [ + `fn main() {`, + ` println!("Bam);`, + `}`, + ].join('\n'), + }, function(buffer) { + expect(buffer.stderr).to.contain('unterminated double quote'); + done(); + }); + }); + + it('should handle broken and unused code', function(done) { + runner.run({ + language: 'rust', + setup: ` + use std::thread; + use std::sync::mpsc::channel; + `, code: ` + fn async_thingo() { + let (tx, rx) = channel(); + thread::spawn(move|| { + tx.send(10).unwrap(); + }); + } + + fn unused_func() { + println!("Never called"); + } + + fn broken_func() { + println!("This is broken..."; + } + fn doubler(n: i32) -> i32 { n * 2 } `, fixture: ` - #[test] - fn doubler_success() { - assert_eq!(doubler(2),4); + #[test] + fn doubler_works() { + assert_eq!(doubler(2),4); + } + `, + testFramework: 'rust' + }, function(buffer) { + expect(buffer.stderr).to.contain(`unclosed delimiter`); + done(); + }); + }); + + it('should ignore unused code warnings', function(done) { + runner.run({ + language: 'rust', + setup: ` + use std::thread; + use std::sync::mpsc::channel; + `, + code: ` + fn async_thingo() { + let (tx, rx) = channel(); + thread::spawn(move|| { + tx.send(10).unwrap(); + }); } - - #[test] - fn doubler_failure() { - assert_eq!(doubler(2),3); + + fn unused_func() { + println!("Never called"); + } + + fn doubler(n: i32) -> i32 { + n * 2 } `, + fixture: ` + #[test] + fn doubler_works() { + assert_eq!(doubler(2),4); + } + `, testFramework: 'rust' }, function(buffer) { - expect(buffer.stdout).to.contain(`doubler_success`); + expect(buffer.stdout).to.contain(`doubler_works`); expect(buffer.stdout).to.contain(`Test Passed`); - expect(buffer.stdout).to.contain(`doubler_failure`); - expect(buffer.stdout).to.contain(`Test Failed`); + expect(buffer.stderr).to.be.empty; done(); }); }); + it('should handle broken test code', function(done) { + runner.run({ + language: 'rust', + code: 'fn doubler(n: i32) -> i32 { n * 2 }', + fixture: [ + `#[test]`, + `fn doubler_works( {`, + ` assert_eq!(doubler(2),4);`, + `}`, + ].join('\n'), + testFramework: 'rust' + }, function(buffer) { + expect(buffer.stderr).to.contain(`un-closed delimiter`); + done(); + }); + }); }); });