├── images ├── screencast.gif ├── screenshot-1.png └── screenshot-2.png ├── .gitignore ├── example ├── main.py ├── main.rb ├── main.txt ├── main.clj ├── main.go ├── main.lisp ├── main.hs ├── main.c ├── main.cpp ├── Main.java └── main.js ├── roswell └── getac.ros ├── src ├── utils.lisp ├── timer.lisp ├── shell.lisp ├── indent-stream.lisp ├── diff.lisp ├── main.lisp ├── testcase.lisp ├── reporter.lisp ├── cli.lisp └── runner.lisp ├── bookmarklet ├── compile.sh └── main.js ├── CHANGELOG.md ├── bookmarklet.js ├── getac.asd ├── Makefile ├── LICENSE └── README.markdown /images/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fukamachi/getac/HEAD/images/screencast.gif -------------------------------------------------------------------------------- /images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fukamachi/getac/HEAD/images/screenshot-1.png -------------------------------------------------------------------------------- /images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fukamachi/getac/HEAD/images/screenshot-2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.fasl 2 | *.dx32fsl 3 | *.dx64fsl 4 | *.lx32fsl 5 | *.lx64fsl 6 | *.x86f 7 | *~ 8 | .#* 9 | 10 | bin/getac 11 | trivial-gray-streams* 12 | -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | def resolve(): 2 | a = int(input()) 3 | b, c = map(int, input().split()) 4 | s = input() 5 | print(a + b + c, s) 6 | 7 | resolve() 8 | -------------------------------------------------------------------------------- /example/main.rb: -------------------------------------------------------------------------------- 1 | # 整数の入力 2 | a = gets.to_i 3 | # スペース区切りの整数の入力 4 | b,c=gets.chomp.split(" ").map(&:to_i); 5 | # 文字列の入力 6 | s = gets.chomp 7 | # 出力 8 | print("#{a+b+c} #{s}\n") 9 | -------------------------------------------------------------------------------- /example/main.txt: -------------------------------------------------------------------------------- 1 | ==== example1 ==== 2 | 1 3 | 2 3 4 | test 5 | -------------- 6 | 6 test 7 | ==== example2 ==== 8 | 2 9 | 3 4 10 | myonmyon 11 | ------- 12 | 9 myonmyon 13 | -------------------------------------------------------------------------------- /example/main.clj: -------------------------------------------------------------------------------- 1 | (defn solve [] 2 | (let [a (Integer/parseInt (read-line)) 3 | [b c] (map #(Integer/parseInt %) (clojure.string/split (read-line) #" ")) 4 | s (read-line)] 5 | (println (+ a b c) s))) 6 | 7 | (solve) 8 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | var a, b, c int 9 | var s string 10 | fmt.Scanf("%d", &a) 11 | fmt.Scanf("%d %d", &b, &c) 12 | fmt.Scanf("%s", &s) 13 | fmt.Printf("%d %s\n", a+b+c, s) 14 | } 15 | -------------------------------------------------------------------------------- /example/main.lisp: -------------------------------------------------------------------------------- 1 | ;; Code for https://atcoder.jp/contests/abs/tasks/practice_1 2 | 3 | (defun resolve () 4 | (let ((a (read)) 5 | (b (read)) 6 | (c (read)) 7 | (s (read-line))) 8 | (format t "~D ~A~%" (+ a b c) s))) 9 | 10 | (resolve) 11 | -------------------------------------------------------------------------------- /example/main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | main :: IO () 4 | main = do 5 | l1 <- getLine 6 | l2 <- getLine 7 | l3 <- getLine 8 | let 9 | a = read l1 :: Int 10 | [b, c] = map (read :: String -> Int) (words l2) 11 | s = l3 12 | putStr (show (a + b + c)) 13 | putStr " " 14 | putStrLn s 15 | -------------------------------------------------------------------------------- /example/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | int main() 3 | { 4 | int a,b,c; 5 | char s[101]; 6 | // 整数の入力 7 | scanf("%d", &a); 8 | // スペース区切りの整数の入力 9 | scanf("%d %d",&b,&c); 10 | // 文字列の入力 11 | scanf("%s",s); 12 | // 出力 13 | printf("%d %s\n",a+b+c,s); 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace std; 3 | int main() 4 | { 5 | // 整数の入力 6 | int a; 7 | cin >> a; 8 | // スペース区切りの整数の入力 9 | int b,c; 10 | cin >> b >> c; 11 | // 文字列の入力 12 | string s; 13 | cin >> s; 14 | // 出力 15 | cout << (a+b+c) << " " << s << endl; 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /example/Main.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | public class Main { 3 | public static void main(String[] args){ 4 | Scanner sc = new Scanner(System.in); 5 | // 整数の入力 6 | int a = sc.nextInt(); 7 | // スペース区切りの整数の入力 8 | int b = sc.nextInt(); 9 | int c = sc.nextInt(); 10 | // 文字列の入力 11 | String s = sc.next(); 12 | // 出力 13 | System.out.println((a+b+c) + " " + s); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /roswell/getac.ros: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #|-*- mode:lisp -*-|# 3 | #| 4 | exec ros -Q -m getac -L sbcl-bin -- $0 "$@" 5 | |# 6 | (progn ;;init forms 7 | (ros:ensure-asdf) 8 | #+quicklisp (ql:quickload '(#:getac) :silent t)) 9 | 10 | (defpackage #:getac/roswell/getac.ros 11 | (:use #:cl)) 12 | (in-package #:getac/roswell/getac.ros) 13 | 14 | (defun main (&rest argv) 15 | (apply #'getac/cli:cli-main argv)) 16 | ;;; vim: set ft=lisp lisp: 17 | -------------------------------------------------------------------------------- /src/utils.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/utils 2 | (:use #:cl) 3 | (:export #:normalize-pathname)) 4 | (in-package #:getac/utils) 5 | 6 | (defun normalize-pathname (filename &optional allow-directory) 7 | (let ((file (probe-file filename))) 8 | (cond 9 | ((null file) 10 | (error "File not exists: ~A" filename)) 11 | ((and (uiop:directory-pathname-p file) 12 | (not allow-directory)) 13 | (error "Is a directory: ~A" filename)) 14 | (t file)))) 15 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | // inputに入力データ全体が入る 2 | function Main(input) { 3 | // 1行目がinput[0], 2行目がinput[1], …に入る 4 | input = input.split("\n"); 5 | tmp = input[1].split(" "); 6 | //文字列から10進数に変換するときはparseIntを使います 7 | var a = parseInt(input[0], 10); 8 | var b = parseInt(tmp[0], 10); 9 | var c = parseInt(tmp[1], 10); 10 | var s = input[2]; 11 | //出力 12 | console.log('%d %s',a+b+c,s); 13 | } 14 | //*この行以降は編集しないでください(標準入出力から一度に読み込み、Mainを呼び出します) 15 | Main(require("fs").readFileSync("/dev/stdin", "utf8")); 16 | -------------------------------------------------------------------------------- /bookmarklet/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$(dirname $0) 4 | FILE="$DIR/main.js" 5 | COMPILE_TO="bookmarklet.js" 6 | echo "Compiling " $FILE"..." 7 | 8 | echo -n "javascript:(function(){" > $COMPILE_TO 9 | curl --data-urlencod "js_code@$FILE" -d compilation_level=SIMPLE_OPTIMIZATIONS -d output_format=text -d output_info=compiled_code https://closure-compiler.appspot.com/compile | tr '\n' ' ' >> $COMPILE_TO 10 | echo -n 'main()})()' >> $COMPILE_TO 11 | 12 | echo "Successfully processed into $COMPILE_TO!" 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # getac Changelog 2 | 3 | ## 2019-01-27, v0.9.2 4 | 5 | * Allow to read test cases from a directory containing separated input/output files. 6 | * bugfix: Fix to print standard error while execution. (reported by @nganhkhoa at [#5](https://github.com/fukamachi/getac/issues/5)) 7 | 8 | ## 2019-12-31, v0.9.1 9 | 10 | * Add Haskell support (thanks to @nganhkhoa) 11 | * Include bookmarklet.js to generate testcases (Experimental) 12 | * Print also the versions of Lisp and ASDF by 'getac --version' 13 | * bugfix: Ensure compilation/execution processes are terminated even when TLE. 14 | * bugfix: Fix a TYPE-ERROR if the testcase ends with a newline. 15 | 16 | ## 2019-12-20, v0.9.0 17 | 18 | * First major release 19 | -------------------------------------------------------------------------------- /src/timer.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/timer 2 | (:use #:cl) 3 | (:export #:max-execution-time* 4 | #:with-deadline 5 | #:deadline-timeout 6 | #:deadline-timeout-seconds)) 7 | (in-package #:getac/timer) 8 | 9 | (defparameter *max-execution-time* 10) ;; in seconds 10 | 11 | (define-condition deadline-timeout (error) 12 | ((seconds :initarg :seconds 13 | :reader deadline-timeout-seconds))) 14 | 15 | (defmacro with-deadline (&body body) 16 | #+sbcl 17 | `(handler-bind ((sb-sys:deadline-timeout 18 | (lambda (e) 19 | (declare (ignore e)) 20 | (error 'deadline-timeout :seconds *max-execution-time*)))) 21 | (sb-sys:with-deadline (:seconds *max-execution-time*) 22 | ,@body)) 23 | #-sbcl 24 | `(progn ,@body)) 25 | -------------------------------------------------------------------------------- /bookmarklet.js: -------------------------------------------------------------------------------- 1 | javascript:(function(){var atcoder=function(){var a="",b=!1;document.querySelectorAll('.lang>span[style*="display: inline"] .part pre[id]').forEach(function(d){if(b)a+="--------\n";else{var c=d.parentNode.querySelector("h3").childNodes[0].textContent;a+="==== "+c+" ====\n"}b=!b;a+=d.innerText});return a},aoj=function(){var a="",b=!1,d=!1,c=document.getElementsByClassName("problemBody")[0];Array.from(c.childNodes).filter(function(a){return 1===a.nodeType}).reverse().forEach(function(c){if(!d)switch(c.tagName.toLowerCase()){case "pre":a= c.innerHTML+a;b=!b;break;case "h2":a&&(a=b?"--------\n"+a:"==== "+c.innerHTML+" ====\n"+a);break;default:a&&(d=!0)}});return a},show=function(a){navigator.userAgent.indexOf("Firefox")?alert(a):prompt("Copy this:",a)},main=function(){"atcoder.jp"===location.hostname?show(atcoder()):"onlinejudge.u-aizu.ac.jp"===location.hostname?show(aoj()):alert("Not supported: "+location.hostname)}; main()})() -------------------------------------------------------------------------------- /src/shell.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/shell 2 | (:use #:cl) 3 | (:export #:run-program)) 4 | (in-package #:getac/shell) 5 | 6 | (defun run-program (command &key input (output :stream)) 7 | (let ((process (uiop:launch-program command 8 | :input input 9 | :output output 10 | :error-output :stream))) 11 | (unwind-protect 12 | (let ((code (uiop:wait-process process))) 13 | (unless (zerop code) 14 | (error 'uiop:subprocess-error 15 | :code code 16 | :command command 17 | :process process)) 18 | (values code 19 | (uiop:process-info-output process) 20 | (uiop:process-info-error-output process))) 21 | (when (uiop:process-alive-p process) 22 | (uiop:terminate-process process :urgent t))))) 23 | -------------------------------------------------------------------------------- /getac.asd: -------------------------------------------------------------------------------- 1 | (defsystem "getac" 2 | :version "0.9.2" 3 | :author "Eitaro Fukamachi" 4 | :license "BSD 3-Clause" 5 | :depends-on ("trivial-gray-streams") 6 | :components ((:module "src" 7 | :depends-on ("utils") 8 | :components 9 | ((:file "main" :depends-on ("others")) 10 | (:file "cli" :depends-on ("main")) 11 | (:module "others" 12 | :pathname "" 13 | :components 14 | ((:file "runner" :depends-on ("timer" "shell")) 15 | (:file "testcase") 16 | (:file "reporter" :depends-on ("runner" "diff" "indent-stream")) 17 | (:file "timer") 18 | (:file "shell") 19 | (:file "diff") 20 | (:file "indent-stream"))))) 21 | (:module "utils" 22 | :pathname "src" 23 | :components ((:file "utils")))) 24 | :description "Unit testing CLI tool for competitive programming") 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | main: build 2 | 3 | TRIVIAL_GRAY_STREAMS_COMMIT = ebd59b1afed03b9dc8544320f8f432fdf92ab010 4 | 5 | trivial-gray-streams.tar.gz: 6 | curl -L https://github.com/trivial-gray-streams/trivial-gray-streams/archive/$(TRIVIAL_GRAY_STREAMS_COMMIT).tar.gz -o trivial-gray-streams.tar.gz 7 | 8 | trivial-gray-streams: trivial-gray-streams.tar.gz 9 | tar zxf trivial-gray-streams.tar.gz 10 | mv trivial-gray-streams-$(TRIVIAL_GRAY_STREAMS_COMMIT) trivial-gray-streams 11 | 12 | bin/getac: trivial-gray-streams 13 | mkdir -p bin 14 | sbcl --eval '(require :asdf)' \ 15 | --eval '(push #P"trivial-gray-streams/" asdf:*central-registry*)' \ 16 | --eval '(push #P"./" asdf:*central-registry*)' \ 17 | --eval '(asdf:load-system :getac)' \ 18 | --eval '(sb-ext:save-lisp-and-die "bin/getac" :toplevel (function getac/cli:main) :executable t :save-runtime-options t)' 19 | 20 | .PHONY: build install uninstall clean 21 | 22 | build: bin/getac 23 | 24 | install: 25 | cp bin/getac /usr/local/bin 26 | 27 | uninstall: 28 | rm /usr/local/bin/getac 29 | 30 | clean: 31 | rm -f bin/getac trivial-gray-streams.tar.gz 32 | rm -rf trivial-gray-streams 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Eitaro Fukamachi 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /bookmarklet/main.js: -------------------------------------------------------------------------------- 1 | const atcoder = function() { 2 | let testcases = '', toggle = false; 3 | document.querySelectorAll('.lang>span[style*="display: inline"] .part pre[id]').forEach(function(node) { 4 | if (toggle) { 5 | testcases += '--------\n'; 6 | } 7 | else { 8 | let header = node.parentNode.querySelector('h3').childNodes[0].textContent; 9 | testcases += '==== ' + header + ' ====\n'; 10 | } 11 | toggle = !toggle; 12 | testcases += node.innerText; 13 | }) 14 | return testcases; 15 | }; 16 | 17 | let aoj = function() { 18 | let testcases = '', isOutput = false, isDone = false; 19 | const problemBody = document.getElementsByClassName('problemBody')[0]; 20 | Array.from(problemBody.childNodes).filter(function(node) { return node.nodeType === 1 }).reverse().forEach(function(node) { 21 | if (!isDone) { 22 | const tagName = node.tagName.toLowerCase(); 23 | switch (tagName) { 24 | case 'pre': 25 | testcases = node.innerHTML + testcases; 26 | isOutput = !isOutput 27 | break; 28 | case 'h2': 29 | if (testcases) { 30 | if (isOutput) { 31 | testcases = '--------\n' + testcases; 32 | } 33 | else { 34 | testcases = '==== ' + node.innerHTML + ' ====\n' + testcases; 35 | } 36 | } 37 | break; 38 | default: 39 | if (testcases) { 40 | isDone = true; 41 | } 42 | } 43 | } 44 | }); 45 | return testcases; 46 | }; 47 | 48 | const show = function(text) { 49 | if (navigator.userAgent.indexOf('Firefox')) { 50 | alert(text); 51 | } 52 | else { 53 | prompt('Copy this:', text); 54 | } 55 | }; 56 | 57 | const main = function() { 58 | if (location.hostname === 'atcoder.jp') { 59 | show(atcoder()); 60 | } 61 | else if (location.hostname === 'onlinejudge.u-aizu.ac.jp') { 62 | show(aoj()); 63 | } 64 | else { 65 | alert('Not supported: ' + location.hostname); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/indent-stream.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/indent-stream 2 | (:use #:cl 3 | #:trivial-gray-streams) 4 | (:export #:indent-stream 5 | #:make-indent-stream 6 | #:stream-fresh-line-p 7 | #:with-indent)) 8 | (in-package #:getac/indent-stream) 9 | 10 | (defclass indent-stream (trivial-gray-stream-mixin 11 | fundamental-character-output-stream) 12 | ((stream :initarg :stream 13 | :accessor stream-real-stream) 14 | (level :initarg :level 15 | :initform 0 16 | :accessor stream-indent-level) 17 | (prefix :initarg :prefix 18 | :initform nil 19 | :accessor stream-prefix) 20 | 21 | (fresh-line-p :initform t 22 | :accessor stream-fresh-line-p))) 23 | 24 | (defun make-indent-stream (stream &rest initargs &key level prefix) 25 | (declare (ignore level prefix)) 26 | (apply #'make-instance 'indent-stream 27 | :stream stream 28 | initargs)) 29 | 30 | (defmacro with-indent ((stream level) &body body) 31 | (let ((g-level (gensym "LEVEL")) 32 | (g-stream (gensym "STREAM"))) 33 | `(let ((,g-level ,level) 34 | (,g-stream ,stream)) 35 | (incf (stream-indent-level ,g-stream) ,g-level) 36 | (unwind-protect (progn ,@body) 37 | (decf (stream-indent-level ,g-stream) ,g-level))))) 38 | 39 | (defun new-line-char-p (char) 40 | (and (member char '(#\Newline #\Linefeed #\Return) :test #'char=) 41 | t)) 42 | 43 | (defmethod stream-write-char ((stream indent-stream) char) 44 | (let ((*standard-output* (stream-real-stream stream))) 45 | (cond 46 | ((new-line-char-p char) 47 | (write-char char) 48 | (setf (stream-fresh-line-p stream) t)) 49 | ((stream-fresh-line-p stream) 50 | (write-string 51 | (make-string (stream-indent-level stream) :initial-element #\Space)) 52 | (when (stream-prefix stream) 53 | (write-string (stream-prefix stream))) 54 | (write-char char) 55 | (setf (stream-fresh-line-p stream) nil)) 56 | (t 57 | (write-char char))))) 58 | 59 | (defmethod stream-line-column ((stream indent-stream)) 60 | (+ (stream-indent-level stream) 61 | (length (stream-prefix stream)) 62 | (call-next-method))) 63 | 64 | (defmethod stream-start-line-p ((stream indent-stream)) 65 | (stream-fresh-line-p stream)) 66 | 67 | (defmethod stream-finish-output ((stream indent-stream)) 68 | (stream-finish-output (stream-real-stream stream))) 69 | 70 | (defmethod stream-force-output ((stream indent-stream)) 71 | (stream-force-output (stream-real-stream stream))) 72 | 73 | (defmethod stream-clear-output ((stream indent-stream)) 74 | (stream-clear-output (stream-real-stream stream))) 75 | -------------------------------------------------------------------------------- /src/diff.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/diff 2 | (:use #:cl) 3 | (:export #:edit-operations)) 4 | (in-package #:getac/diff) 5 | 6 | (declaim (type hash-table *memoized*)) 7 | (defvar *memoized*) 8 | 9 | (declaim (ftype (function (simple-string simple-string fixnum fixnum fixnum fixnum) (values list fixnum)) 10 | %compute-edit-operations compute-edit-operations)) 11 | 12 | (defun %compute-edit-operations (a b start1 start2 end1 end2) 13 | (declare (optimize (speed 3) (safety 0) (debug 0)) 14 | (simple-string a b) 15 | (fixnum start1 start2 end1 end2)) 16 | (let ((a-len (- end1 start1)) 17 | (b-len (- end2 start2))) 18 | (declare (fixnum a-len b-len)) 19 | (cond 20 | ((= 0 a-len) 21 | (values (make-list b-len :initial-element #\A) 22 | b-len)) 23 | ((= 0 b-len) 24 | (values (make-list a-len :initial-element #\D) 25 | a-len)) 26 | (t 27 | (if (char= (aref a start1) (aref b start2)) 28 | (multiple-value-bind (ops distance) 29 | (compute-edit-operations a b (1+ start1) (1+ start2) end1 end2) 30 | (values (cons #\M ops) distance)) 31 | (multiple-value-bind (ops1 d1) 32 | (compute-edit-operations a b start1 (1+ start2) end1 end2) 33 | (when (zerop d1) 34 | (return-from %compute-edit-operations 35 | (values (cons #\I ops1) 1))) 36 | (multiple-value-bind (ops2 d2) 37 | (compute-edit-operations a b (1+ start1) start2 end1 end2) 38 | (when (zerop d2) 39 | (return-from %compute-edit-operations 40 | (values (cons #\D ops2) 1))) 41 | (multiple-value-bind (ops3 d3) 42 | (compute-edit-operations a b (1+ start1) (1+ start2) end1 end2) 43 | (let ((min-distance (min d1 d2 d3))) 44 | (declare (fixnum min-distance)) 45 | (values (cond 46 | ((= d1 min-distance) 47 | (cons #\I ops1)) 48 | ((= d2 min-distance) 49 | (cons #\D ops2)) 50 | (t (cons #\R ops3))) 51 | (1+ min-distance))))))))))) 52 | 53 | (defun compute-edit-operations (a b start1 start2 end1 end2) 54 | (declare (optimize (speed 3) (safety 0) (debug 0)) 55 | (simple-string a b) 56 | (fixnum start1 start2 end1 end2)) 57 | "Call %compute-edit-operations with memoization." 58 | (let ((result (gethash (list start1 start2 end1 end2) *memoized*))) 59 | (declare (list result)) 60 | (apply #'values (or result 61 | (setf (gethash (list start1 start2 end1 end2) 62 | *memoized*) 63 | (multiple-value-list (%compute-edit-operations a b start1 start2 end1 end2))))))) 64 | 65 | (declaim (ftype (function (simple-string simple-string) (values list fixnum)) 66 | edit-operations)) 67 | 68 | (defun edit-operations (a b) 69 | (declare (optimize (speed 3) (safety 2) (debug 2)) 70 | (simple-string a b)) 71 | "Return the operations to make A be the same as B as a list. 72 | 73 | Each operations are a character which is one of 4 characters -- #\A for addition, #\D for deletion, #\R for replacement and #\M for matched ones." 74 | (let ((end1 (length a)) 75 | (end2 (length b)) 76 | (*memoized* (make-hash-table :test 'equal))) 77 | (declare (fixnum end1 end2)) 78 | (compute-edit-operations a b 0 0 end1 end2))) 79 | -------------------------------------------------------------------------------- /src/main.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac 2 | (:use #:cl 3 | #:getac/utils) 4 | (:import-from #:getac/runner 5 | #:detect-filetype 6 | #:compile-main-file 7 | #:make-execution-handler 8 | #:execution-error 9 | #:compilation-error) 10 | (:import-from #:getac/testcase 11 | #:read-from-file 12 | #:default-testcase) 13 | (:import-from #:getac/reporter 14 | #:*enable-colors* 15 | #:report-accepted 16 | #:report-wrong-answer 17 | #:report-compilation-error 18 | #:report-runtime-error 19 | #:report-time-limit-exceeded 20 | #:report-canceled 21 | #:print-summary) 22 | (:import-from #:getac/timer 23 | #:*max-execution-time* 24 | #:deadline-timeout 25 | #:deadline-timeout-seconds) 26 | (:export #:*enable-colors* 27 | #:*max-execution-time* 28 | #:*default-timeout* 29 | #:run)) 30 | (in-package #:getac) 31 | 32 | (defparameter *default-timeout* 2) 33 | 34 | (defun run (file &key test filetype (fail-fast t) (timeout *default-timeout*)) 35 | (let* ((file (normalize-pathname file)) 36 | (filetype (or filetype 37 | (detect-filetype file))) 38 | (testcase (or test 39 | (default-testcase file)))) 40 | (let* ((compile-to (handler-case (compile-main-file file filetype) 41 | (compilation-error (e) 42 | (report-compilation-error e) 43 | (return-from run nil)))) 44 | (handler (make-execution-handler file filetype :compile-to compile-to)) 45 | (test-cases (read-from-file testcase)) 46 | (all-test-count (length test-cases)) 47 | (passed-count 0) 48 | (failed-count 0)) 49 | (format t "~&Running ~A test cases...~2%" all-test-count) 50 | (loop for (test-name input expected) in test-cases 51 | do (handler-case 52 | (multiple-value-bind (result took-ms) 53 | (funcall handler input) 54 | (check-type result string) 55 | (cond 56 | ((not (equal result expected)) 57 | (report-wrong-answer test-name input expected result took-ms) 58 | (incf failed-count) 59 | (when fail-fast 60 | (return)) 61 | (write-char #\Newline)) 62 | ((< (* timeout 1000) took-ms) 63 | (report-time-limit-exceeded test-name (/ took-ms 1000.0)) 64 | (incf failed-count) 65 | (when fail-fast 66 | (return)) 67 | (write-char #\Newline)) 68 | (t (incf passed-count) 69 | (report-accepted test-name took-ms)))) 70 | (execution-error (e) 71 | (report-runtime-error test-name input e) 72 | (incf failed-count) 73 | (when fail-fast 74 | (return))) 75 | (deadline-timeout (e) 76 | (report-canceled test-name input (deadline-timeout-seconds e)) 77 | (incf failed-count) 78 | (when fail-fast 79 | (return))))) 80 | (print-summary passed-count failed-count (- all-test-count 81 | passed-count 82 | failed-count)) 83 | (= failed-count 0)))) 84 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # getac 2 | 3 | Quick unit testing CLI tool for competitive programming. 4 | 5 | ![Screenshot](images/screencast.gif) 6 | 7 | ## Usage 8 | 9 | ``` 10 | $ getac -h 11 | Usage: getac [options] 12 | 13 | OPTIONS: 14 | -t, --test= 15 | Specify a file to read test cases. (Default: .txt) 16 | -f, --filetype= 17 | File type to test. The default will be detected by the file extension. 18 | -T, --timeout= 19 | Time limit for each test cases. (Default: 2) 20 | --disable-colors 21 | Turn off colors. 22 | -F, --no-fail-fast 23 | Don't quit when a failure. 24 | -V, --version 25 | Print version. 26 | -h, --help 27 | Show help. 28 | 29 | # Run with the default Python 30 | $ getac main.py 31 | 32 | # Specify test cases 33 | $ getac -t test-cases.in main.py 34 | 35 | # Run with PyPy3 36 | $ getac -f pypy3 main.py 37 | ``` 38 | 39 | ## Installation 40 | 41 | ### via Homebrew (for macOS/Linux) 42 | 43 | Install [Homebrew](https://brew.sh/) if not already installed. 44 | 45 | ``` 46 | $ brew tap fukamachi/getac 47 | $ brew install getac 48 | ``` 49 | 50 | ### Manually 51 | 52 | Install [SBCL](http://sbcl.org/) if not already installed. 53 | 54 | ``` 55 | $ git clone https://github.com/fukamachi/getac 56 | $ cd getac 57 | $ make 58 | $ sudo make install 59 | ``` 60 | 61 | ## Getting started 62 | 63 | 1. Write your code in a file (ex. `main.py`) 64 | 65 | ```python 66 | a = int(input()) 67 | b, c = map(int, input().split()) 68 | s = input() 69 | print(a + b + c, s) 70 | ``` 71 | 72 | 2. Write your test cases in "*.txt" (ex. `main.txt`) 73 | 74 | ``` 75 | ==== example1 ==== 76 | 1 77 | 2 3 78 | test 79 | -------------- 80 | 6 test 81 | ==== example2 ==== 82 | 2 83 | 3 4 84 | myonmyon 85 | ------- 86 | 9 myonmyon 87 | ``` 88 | 89 | See [Format of test cases](#format-of-test-cases) for getting its syntax. 90 | 91 | 3. Run `getac main.py` 92 | 93 | ![Screenshot of successed tests](images/screenshot-1.png) 94 | 95 | If some test cases are failed, it shows 'WA' (wrong answer). 96 | 97 | ![Screenshot of failed tests](images/screenshot-2.png) 98 | 99 | ## Format of test cases 100 | 101 | ### Single .txt file 102 | 103 | Let's start with this very minimal example which contains a single test case: 104 | 105 | ``` 106 | Input texts are here... 107 | ------------------ 108 | Expected result is here... 109 | ``` 110 | 111 | The input text and its expected results are separated with `----` (more than 4 hyphens). 112 | 113 | When you include multiple test cases in the same file, those have to be divided with `====` (more than 4 equal signs). 114 | 115 | ``` 116 | Input texts are here... 117 | ------------------ 118 | Expected result is here... 119 | ================== 120 | Second test case 121 | ------------------ 122 | Expected result is here again... 123 | ``` 124 | 125 | The name of each test cases can be written in the middle of separators, like `==== example1 ====`. 126 | 127 | Here's the full example: 128 | 129 | ``` 130 | ==== example1 ==== 131 | Input texts are here... 132 | ------------------ 133 | Expected result is here... 134 | ==== example2 ==== 135 | Second test case 136 | ------------------ 137 | Expected result is here again... 138 | ``` 139 | 140 | ### Separated input/output files 141 | 142 | If the single test file couldn't find, getac tries to get from `./test/` and `./tests/` directory, which contains test cases as separated input/output files. 143 | 144 | The directory structure is like this: 145 | 146 | ``` 147 | . 148 | ├── main.lisp 149 | └── tests 150 | ├── sample-1.in 151 | ├── sample-1.out 152 | ├── sample-2.in 153 | └── sample-2.out 154 | ``` 155 | 156 | ## Supported languages 157 | 158 | * C 159 | * C++ 160 | * Clojure 161 | * Common Lisp 162 | * Go 163 | * Haskell 164 | * Java 165 | * Node.js 166 | * Python 167 | * Ruby 168 | * Scheme 169 | 170 | ## Author 171 | 172 | * Eitaro Fukamachi (e.arrows@gmail.com) 173 | 174 | ## Copyright 175 | 176 | Copyright (c) 2019 Eitaro Fukamachi (e.arrows@gmail.com) 177 | 178 | ## License 179 | 180 | See [LICENSE](LICENSE). 181 | -------------------------------------------------------------------------------- /src/testcase.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/testcase 2 | (:use #:cl 3 | #:getac/utils) 4 | (:export #:read-from-stream 5 | #:read-from-file 6 | #:default-testcase-file)) 7 | (in-package #:getac/testcase) 8 | 9 | (defparameter *testcase-file-extension* "txt") 10 | 11 | (defun header-line-p (line) 12 | (let ((start (position-if (lambda (x) 13 | (char/= x #\=)) 14 | line))) 15 | (or (and (null start) 16 | (<= 4 (length line)) 17 | (values t nil)) 18 | (and (not (null start)) 19 | (<= 2 start) 20 | (let ((end (position-if (lambda (x) 21 | (char= x #\=)) 22 | line 23 | :start start))) 24 | (and end 25 | (<= 2 (- (length line) end)) 26 | (not (find-if (lambda (x) (char/= x #\=)) line :start end)) 27 | (values t (string-trim '(#\Space #\Tab) (subseq line start end))))))))) 28 | 29 | (defun delimiter-line-p (line) 30 | (and (<= 4 (length line)) 31 | (every (lambda (x) (char= x #\-)) line))) 32 | 33 | (defparameter *default-test-name* "test case ~D") 34 | 35 | (defun read-from-stream (stream) 36 | (let ((eof nil) 37 | (results '()) 38 | (header-line (read-line stream nil nil)) 39 | (gen-test-count 0)) 40 | (loop 41 | (when eof 42 | (return)) 43 | (multiple-value-bind (headerp test-name) 44 | (header-line-p header-line) 45 | (let ((test-name (or test-name 46 | (format nil *default-test-name* (incf gen-test-count)))) 47 | (input 48 | (let ((buffer (make-string-output-stream))) 49 | (unless headerp 50 | (format buffer "~A~%" header-line)) 51 | (loop 52 | (let ((line (read-line stream nil nil))) 53 | (cond 54 | ((null line) 55 | (setf eof t) 56 | (return)) 57 | ((delimiter-line-p line) 58 | (return)) 59 | (t (format buffer "~A~%" line))))) 60 | (get-output-stream-string buffer)))) 61 | (unless eof 62 | (let ((buffer (make-string-output-stream))) 63 | (loop 64 | (let ((line (read-line stream nil nil))) 65 | (cond 66 | ((null line) 67 | (push (list test-name input (get-output-stream-string buffer)) 68 | results) 69 | (setf eof t) 70 | (return)) 71 | ((header-line-p line) 72 | (push (list test-name input (get-output-stream-string buffer)) 73 | results) 74 | (setf header-line line) 75 | (return)) 76 | (t 77 | (format buffer "~A~%" line)))))))))) 78 | (nreverse results))) 79 | 80 | (defun read-from-file (test-file-or-directory) 81 | (let ((test-file-or-directory (normalize-pathname test-file-or-directory t))) 82 | (if (uiop:file-pathname-p test-file-or-directory) 83 | (uiop:with-input-file (in test-file-or-directory) 84 | (read-from-stream in)) 85 | (let ((input-files (uiop:directory-files test-file-or-directory "*.in"))) 86 | (loop for input-file in input-files 87 | for output-file = (make-pathname :name (pathname-name input-file) 88 | :type "out" 89 | :defaults input-file) 90 | if (not (uiop:file-exists-p output-file)) 91 | do (warn "'~A' doesn't have corresponding '.out' file. Skipped." (uiop:native-namestring input-file)) 92 | else 93 | collect (list (pathname-name input-file) 94 | (uiop:read-file-string input-file) 95 | (uiop:read-file-string output-file))))))) 96 | 97 | (defun default-testcase (file) 98 | (let ((file (normalize-pathname file))) 99 | (block nil 100 | (let ((test-file (make-pathname :name (pathname-name file) 101 | :type *testcase-file-extension* 102 | :defaults file))) 103 | (when (uiop:file-exists-p test-file) 104 | (return test-file))) 105 | (let ((test-directory (merge-pathnames #P"test/" 106 | (uiop:pathname-directory-pathname file)))) 107 | (when (uiop:directory-exists-p test-directory) 108 | (return test-directory))) 109 | (let ((tests-directory (merge-pathnames #P"tests/" 110 | (uiop:pathname-directory-pathname file)))) 111 | (when (uiop:directory-exists-p tests-directory) 112 | (return tests-directory))) 113 | (error "Couldn't find test case file(s).")))) 114 | -------------------------------------------------------------------------------- /src/reporter.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/reporter 2 | (:use #:cl) 3 | (:import-from #:getac/runner 4 | #:subprocess-error-command 5 | #:subprocess-error-output) 6 | (:import-from #:getac/diff 7 | #:edit-operations) 8 | (:import-from #:getac/indent-stream 9 | #:make-indent-stream 10 | #:stream-fresh-line-p 11 | #:new-line-char-p 12 | #:with-indent) 13 | (:export #:*enable-colors* 14 | #:report-accepted 15 | #:report-wrong-answer 16 | #:report-runtime-error 17 | #:report-compilation-error 18 | #:report-time-limit-exceeded 19 | #:report-canceled 20 | #:print-summary)) 21 | (in-package #:getac/reporter) 22 | 23 | (defvar *enable-colors* 24 | (not 25 | (or (equal (uiop:getenv "EMACS") "t") 26 | (uiop:getenv "INSIDE_EMACS")))) 27 | 28 | (defparameter *background-color-codes* 29 | '((:green . 42) 30 | (:red . 41) 31 | (:white . 47))) 32 | 33 | (defparameter *color-codes* 34 | '((:green . 32) 35 | (:red . 31) 36 | (:aqua . 36) 37 | (:gray . 90))) 38 | 39 | (defun start-color (color &optional stream) 40 | (when *enable-colors* 41 | (let ((code (cdr (assoc color *color-codes*)))) 42 | (unless code 43 | (error "Unsupported color: ~A" color)) 44 | (format stream "~C[~Am" #\Esc code))) 45 | (values)) 46 | 47 | (defun reset-color (&optional stream) 48 | (when *enable-colors* 49 | (format stream "~C[0m" #\Esc)) 50 | (values)) 51 | 52 | (defun color-text (color text) 53 | (if (= (length text) 0) 54 | text 55 | (with-output-to-string (s) 56 | (start-color color s) 57 | (format s text) 58 | (reset-color s)))) 59 | 60 | (defun print-badge (label color &optional (stream *standard-output*)) 61 | (check-type label string) 62 | (check-type stream stream) 63 | (let ((code (cdr (assoc color *background-color-codes*)))) 64 | (unless code 65 | (error "Unsupported color: ~S" color)) 66 | (if *enable-colors* 67 | (format stream "~C[30;~Am ~A ~C[0m" #\Esc code label #\Esc) 68 | (format stream "[ ~A ]" label)))) 69 | 70 | (defun print-first-line (label color &optional test-name took-ms (stream *standard-output*)) 71 | (format stream "~& ") 72 | (print-badge label color stream) 73 | (when test-name 74 | (format stream " ~A" test-name)) 75 | (when took-ms 76 | (format stream " ~A" (color-text :gray (format nil "(~A ms)" took-ms)))) 77 | (fresh-line stream)) 78 | 79 | (defun report-accepted (test-name took-ms) 80 | (print-first-line "AC" :green test-name took-ms)) 81 | 82 | (defun diff (a b) 83 | (let ((ops (edit-operations a b))) 84 | (let ((stream (make-indent-stream *standard-output* 85 | :prefix (color-text :red "- "))) 86 | (ops (remove #\D ops))) 87 | (with-indent (stream +3) 88 | (let ((color nil)) 89 | (loop 90 | for op = (pop ops) 91 | for ch across b 92 | if (or (null op) 93 | (char= op #\M)) 94 | do (when color 95 | (reset-color stream) 96 | (setf color nil)) 97 | (princ ch stream) 98 | else 99 | do (unless color 100 | (start-color :red stream) 101 | (setf color t)) 102 | (princ ch stream)) 103 | (when color 104 | (reset-color stream))) 105 | (when (or (zerop (length b)) 106 | (not (new-line-char-p (aref b (1- (length b)))))) 107 | (format stream "~&~A~%" 108 | (color-text :gray "[no newline at the end]"))))) 109 | (format t "~&") 110 | (let ((stream (make-indent-stream *standard-output* 111 | :prefix (color-text :green "+ "))) 112 | (ops (remove #\I ops))) 113 | (with-indent (stream +3) 114 | (let ((color nil)) 115 | (loop 116 | for op = (pop ops) 117 | for ch across a 118 | if (or (null op) 119 | (char= op #\M)) 120 | do (when color 121 | (reset-color stream) 122 | (setf color nil)) 123 | (princ ch stream) 124 | else 125 | do (unless color 126 | (start-color :green stream) 127 | (setf color t)) 128 | (princ ch stream)) 129 | (when color 130 | (reset-color stream))) 131 | (when (or (zerop (length a)) 132 | (not (new-line-char-p (aref a (1- (length a)))))) 133 | (format stream "~&~A~%" 134 | (color-text :gray "[no newline at the end]"))))) 135 | (format t "~&"))) 136 | 137 | (defun %print-input (input) 138 | (format t "~2&") 139 | (let ((stream (make-indent-stream *standard-output* :level 3))) 140 | (princ input stream))) 141 | 142 | (defun report-wrong-answer (test-name input expected actual took-ms) 143 | (print-first-line "WA" :red test-name took-ms) 144 | (%print-input input) 145 | (format t "~2& ~A ~A~2%" 146 | (color-text :red "- actual") 147 | (color-text :green "+ expected")) 148 | (diff expected actual)) 149 | 150 | (defun report-compilation-error (error) 151 | (print-first-line "CE" :red "Compilation failed.") 152 | (let ((stream (make-indent-stream *standard-output*))) 153 | (with-indent (stream +3) 154 | (format stream "~&~A~2%" 155 | (color-text :gray 156 | (format nil "While executing ~{~A~^ ~}" 157 | (subprocess-error-command error))))) 158 | (with-indent (stream +5) 159 | (format stream "~A~%" (color-text :gray (subprocess-error-output error)))))) 160 | 161 | (defun report-runtime-error (test-name input error) 162 | (print-first-line "RE" :red test-name) 163 | (%print-input input) 164 | (let ((stream (make-indent-stream *standard-output*))) 165 | (with-indent (stream +3) 166 | (format stream "~2&~A~2%" 167 | (color-text :gray 168 | (format nil "While executing ~{~A~^ ~}" 169 | (subprocess-error-command error))))) 170 | (with-indent (stream +5) 171 | (format stream "~A~%" (color-text :gray (subprocess-error-output error)))))) 172 | 173 | (defun report-time-limit-exceeded (test-name seconds) 174 | (print-first-line "TLE" :red test-name) 175 | (format t "~& ~A~%" 176 | (color-text :gray 177 | (format nil "Took ~A seconds and time limit exceeded" seconds)))) 178 | 179 | (defun report-canceled (test-name input seconds) 180 | (print-first-line "TLE" :red test-name) 181 | (%print-input input) 182 | (format t "~2& ~A~%" 183 | (color-text :gray 184 | (format nil "Canceled after ~A seconds" seconds)))) 185 | 186 | (defun print-summary (passed-count failed-count skipped-count) 187 | (princ 188 | (if (= failed-count 0) 189 | (color-text :green 190 | (format nil "~2&✓ All ~D test case~:*~P passed~%" 191 | passed-count)) 192 | (color-text :red 193 | (format nil "~2&× ~D of ~D test case~:*~P failed~%" 194 | failed-count 195 | (+ passed-count failed-count))))) 196 | (unless (= skipped-count 0) 197 | (princ (color-text :aqua 198 | (format nil "● ~D test case~:*~P skipped~%" skipped-count))))) 199 | -------------------------------------------------------------------------------- /src/cli.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/cli 2 | (:use #:cl) 3 | (:import-from #:getac 4 | #:run) 5 | (:export #:option 6 | #:defoption 7 | #:option-name 8 | #:option-handler 9 | #:find-option 10 | #:cli-error 11 | #:invalid-option 12 | #:missing-option-value 13 | #:print-options-usage 14 | #:parse-argv 15 | #:cli-main 16 | #:main)) 17 | (in-package #:getac/cli) 18 | 19 | (define-condition cli-error (error) ()) 20 | (define-condition invalid-option (cli-error) 21 | ((name :initarg :name)) 22 | (:report (lambda (error stream) 23 | (format stream "Invalid option: ~A" (slot-value error 'name))))) 24 | (define-condition missing-option-value (cli-error) 25 | ((name :initarg :name)) 26 | (:report (lambda (error stream) 27 | (format stream "Missing value for the option: ~A" (slot-value error 'name))))) 28 | 29 | (defvar *options* '()) 30 | (defvar *short-options* '()) 31 | 32 | (defstruct option 33 | name 34 | docstring 35 | short 36 | lambda-list 37 | handler) 38 | 39 | (defmacro defoption (name (&key short) lambda-list &body body) 40 | (let ((option (gensym "OPTION")) 41 | (g-short (gensym "SHORT"))) 42 | ;; Currently support only zero or one arguments 43 | (assert (or (null lambda-list) 44 | (null (rest lambda-list)))) 45 | (multiple-value-bind (docstring body) 46 | (if (and (rest body) 47 | (stringp (first body))) 48 | (values (first body) (rest body)) 49 | (values nil body)) 50 | `(let* ((,g-short ,short) 51 | (,option (make-option :name ,name 52 | :docstring ,docstring 53 | :short ,g-short 54 | :lambda-list ',lambda-list 55 | :handler (lambda ,lambda-list ,@body)))) 56 | (push (cons (format nil "--~A" ,name) ,option) *options*) 57 | (when ,g-short 58 | (push (cons (format nil "-~A" ,g-short) ,option) *short-options*)) 59 | ,option)))) 60 | 61 | (defun find-option (name &optional errorp) 62 | (let ((option (or (cdr (assoc name *options* :test 'equal)) 63 | (cdr (assoc name *short-options* :test 'equal))))) 64 | (when (and (null option) 65 | errorp) 66 | (error 'invalid-option :name name)) 67 | option)) 68 | 69 | (defun print-usage-of-option (option stream) 70 | (format stream "~& ~@[-~A, ~]--~A~@[=~{<~(~A~)>~^ ~}~]~%~@[ ~A~%~]" 71 | (option-short option) 72 | (option-name option) 73 | (option-lambda-list option) 74 | (option-docstring option))) 75 | 76 | (defun print-options-usage (&optional (stream *error-output*)) 77 | (dolist (option (reverse *options*)) 78 | (print-usage-of-option (cdr option) stream))) 79 | 80 | (defun option-string-p (string) 81 | (and (stringp string) 82 | (<= 2 (length string)) 83 | (char= #\- (aref string 0)))) 84 | 85 | (defun parse-argv (argv) 86 | (loop for arg = (pop argv) 87 | while arg 88 | if (option-string-p arg) 89 | append (let ((=-pos (position #\= arg :start 1))) 90 | (multiple-value-bind (option-name value) 91 | (if =-pos 92 | (values (subseq arg 0 =-pos) 93 | (subseq arg (1+ =-pos))) 94 | (values arg nil)) 95 | (let ((option (find-option option-name t))) 96 | (when (and (option-lambda-list option) 97 | (null value)) 98 | (setf value (pop argv)) 99 | (when (or (null value) 100 | (option-string-p value)) 101 | (error 'missing-option-value 102 | :name option-name))) 103 | (apply (option-handler option) 104 | (and value (list value)))))) into results 105 | else 106 | do (return (values results (cons arg argv))) 107 | finally (return (values results nil)))) 108 | 109 | (defun print-usage (&key (quit t)) 110 | (format *error-output* 111 | "~&Usage: getac [options] ~2%OPTIONS:~%") 112 | (print-options-usage *error-output*) 113 | (when quit 114 | (uiop:quit -1))) 115 | 116 | (defoption "test" (:short "t") (file) 117 | "Specify a file to read test cases. (Default: .txt)" 118 | (list :test file)) 119 | 120 | (defoption "filetype" (:short "f") (type) 121 | "File type to test. The default will be detected by the file extension." 122 | (list :filetype type)) 123 | 124 | (defoption "timeout" (:short "T") (seconds) 125 | #.(format nil "Time limit for each test cases. (Default: ~A)" getac:*default-timeout*) 126 | (let ((sec (handler-case (read-from-string seconds) 127 | (error () 128 | (error "Invalid value for timeout: ~A" seconds))))) 129 | (unless (numberp sec) 130 | (error "Invalid value for timeout: ~A" seconds)) 131 | (list :timeout sec))) 132 | 133 | (defoption "disable-colors" () () 134 | "Turn off colors." 135 | (setf getac:*enable-colors* nil) 136 | (values)) 137 | 138 | (defoption "no-fail-fast" (:short "F") () 139 | "Don't quit when a failure." 140 | (list :fail-fast nil)) 141 | 142 | (defoption "version" (:short "V") () 143 | "Print version." 144 | (format *error-output* "~&getac v~A running on ~A ~A~@[ (with ASDF ~A)~]~%" 145 | (asdf:component-version (asdf:find-system '#:getac)) 146 | (lisp-implementation-type) 147 | (lisp-implementation-version) 148 | #+asdf (asdf:asdf-version) #-asdf nil) 149 | (uiop:quit -1)) 150 | 151 | (defoption "help" (:short "h") () 152 | "Show help." 153 | (print-usage :quit t)) 154 | 155 | (defun cli-main (&rest argv) 156 | (unless argv 157 | (print-usage)) 158 | (handler-case 159 | (multiple-value-bind (options argv) 160 | (handler-case (parse-argv argv) 161 | (cli-error (e) 162 | (format *error-output* "~&~A~%" e) 163 | (print-usage))) 164 | (let ((filename (pop argv))) 165 | (unless filename 166 | (format *error-output* "~&Missing a file to test.~%") 167 | (print-usage)) 168 | (when argv 169 | ;; Allow options after the filename 170 | (multiple-value-bind (more-options argv) 171 | (handler-case (parse-argv argv) 172 | (cli-error (e) 173 | (format *error-output* "~&~A~%" e) 174 | (print-usage))) 175 | (when argv 176 | (format *error-output* "Extra arguments: ~{~A~^ ~}~%" argv) 177 | (print-usage)) 178 | (setf options (append options more-options)))) 179 | 180 | (or (handler-case (apply #'getac:run filename options) 181 | (simple-error (e) 182 | (format *error-output* "~&~A~%" e) 183 | nil) 184 | (error (e) 185 | (format *error-output* "~&~A: ~A~%" (type-of e) e) 186 | nil)) 187 | (uiop:quit -1)))) 188 | #+sbcl (sb-sys:interactive-interrupt () (uiop:quit -1 t)))) 189 | 190 | (defun main (&optional (argv nil argv-supplied-p)) 191 | (let ((argv (if argv-supplied-p 192 | (rest argv) 193 | (or #+sbcl (rest sb-ext:*posix-argv*) 194 | #-sbcl uiop:*command-line-arguments*)))) 195 | (apply #'cli-main argv))) 196 | -------------------------------------------------------------------------------- /src/runner.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:getac/runner 2 | (:use #:cl 3 | #:getac/utils) 4 | (:import-from #:getac/timer 5 | #:with-deadline) 6 | (:import-from #:getac/shell 7 | #:run-program) 8 | (:export #:detect-filetype 9 | #:compile-main-file 10 | #:make-execution-handler 11 | #:subprocess-error 12 | #:execution-error 13 | #:compilation-error 14 | #:subprocess-error-command 15 | #:subprocess-error-output)) 16 | (in-package #:getac/runner) 17 | 18 | (defparameter *cpp-include-locations* 19 | (list #+unix "/usr/include/boost" 20 | #+darwin "/usr/local/include/boost")) 21 | 22 | (defvar *default-commands* 23 | `(#+ros.init 24 | ("lisp" . (("execute" . ("ros" "run" "--" "--script" file)))) 25 | #-ros.init 26 | ("lisp" . (("execute" . ("sbcl" "--script" file)))) 27 | ("python" . (("execute" . ("python" "-B" file)))) 28 | ("python2" . (("execute" . ("python2" "-B" file)))) 29 | ("python3" . (("execute" . ("python3" "-B" file)))) 30 | ("pypy" . (("execute" . ("pypy" file)))) 31 | ("pypy2" . (("execute" . ("pypy2" file)))) 32 | ("pypy3" . (("execute" . ("pypy3" file)))) 33 | ("ruby" . (("execute" . ("ruby" "--disable-gems" file)))) 34 | ("go" . (("compile" . ("go" "build" "-o" compile-to file)) 35 | ("execute" . (compile-to)))) 36 | ("java" . (("compile" . ("javac" "-d" compile-in file)) 37 | ("execute" . ("java" "-cp" compile-in filename)))) 38 | ("javascript" . (("execute" . ("node" file)))) 39 | ("clojure" . (("execute" . ("clj" file)))) 40 | ("c" . (("compile" . ("gcc" "-std=gnu11" "-O2" "-o" compile-to file "-lm")) 41 | ("execute" . (compile-to)))) 42 | ("cpp" . (("compile" . ("g++" "-std=gnu++1y" "-O2" ,@(mapcar (lambda (dir) (format nil "-I~A" dir)) 43 | *cpp-include-locations*) 44 | "-o" compile-to file)) 45 | ("execute" . (compile-to)))) 46 | ("scheme" . (("execute" . ("gosh" file)))) 47 | ("haskell" . (("compile" . ("stack" "ghc" "--" "-o" compile-to "-odir" compile-in "-hidir" compile-in "-O2" file)) 48 | ("execute" . (compile-to)))))) 49 | 50 | (defun render-command (command-template values) 51 | (mapcar (lambda (x) 52 | (typecase x 53 | (symbol 54 | (let ((pair (assoc (symbol-name x) values :test 'equalp :key #'symbol-name))) 55 | (unless pair 56 | (error "Unexpected variable: ~A" x)) 57 | (cdr pair))) 58 | (otherwise (princ-to-string x)))) 59 | command-template)) 60 | 61 | (define-condition subprocess-error (error) 62 | ((command :initarg :command 63 | :reader subprocess-error-command) 64 | (output :initarg :output 65 | :reader subprocess-error-output))) 66 | 67 | (define-condition execution-error (subprocess-error) ()) 68 | (define-condition compilation-error (subprocess-error) ()) 69 | 70 | (defmacro with-took-ms (took-ms &body body) 71 | (let ((before (gensym "BEFORE"))) 72 | `(let ((,before (get-internal-real-time))) 73 | (multiple-value-prog1 (progn ,@body) 74 | (setf ,took-ms 75 | (* (- (get-internal-real-time) ,before) 76 | #.(if (= internal-time-units-per-second 1000) 77 | 1 78 | (/ internal-time-units-per-second 1000.0)))))))) 79 | 80 | (defun execute-code (command-template file input &optional compile-to) 81 | (check-type command-template cons) 82 | (check-type file pathname) 83 | (check-type input string) 84 | (let ((command (render-command command-template 85 | `((file . ,(uiop:native-namestring file)) 86 | (filename . ,(pathname-name file)) 87 | (compile-to . ,(and compile-to 88 | (uiop:native-namestring compile-to))) 89 | (compile-in . ,(and compile-to 90 | (uiop:native-namestring (uiop:pathname-directory-pathname compile-to))))))) 91 | (took-ms 0)) 92 | (with-input-from-string (in input) 93 | (values 94 | (multiple-value-bind (code output errout) 95 | (handler-case (with-took-ms took-ms 96 | (run-program command :input in)) 97 | (uiop:subprocess-error (e) 98 | (let ((process (uiop:subprocess-error-process e))) 99 | (error 'execution-error 100 | :command command 101 | :output (with-output-to-string (s) 102 | (uiop:copy-stream-to-stream 103 | (uiop:process-info-error-output process) 104 | s)))))) 105 | (declare (ignore code)) 106 | (uiop:copy-stream-to-stream errout *error-output*) 107 | (with-output-to-string (s) 108 | (uiop:copy-stream-to-stream output s))) 109 | took-ms)))) 110 | 111 | (defun compile-code (command-template file &optional compile-to) 112 | (check-type command-template cons) 113 | (check-type file pathname) 114 | (check-type compile-to (or null pathname)) 115 | (let* ((compile-to (or compile-to 116 | (uiop:with-temporary-file (:pathname file) file))) 117 | (command (render-command command-template 118 | `((file . ,(uiop:native-namestring file)) 119 | (compile-to . ,(uiop:native-namestring compile-to)) 120 | (compile-in . ,(uiop:native-namestring (uiop:pathname-directory-pathname compile-to))))))) 121 | (handler-case (run-program command 122 | :input :interactive 123 | :output :interactive) 124 | (uiop:subprocess-error (e) 125 | (let ((process (uiop:subprocess-error-process e))) 126 | (error 'compilation-error 127 | :command command 128 | :output (with-output-to-string (s) 129 | (uiop:copy-stream-to-stream 130 | (uiop:process-info-error-output process) 131 | s)))))) 132 | compile-to)) 133 | 134 | (defun detect-filetype (file) 135 | (check-type file pathname) 136 | (let ((type (pathname-type file))) 137 | (cond 138 | ((string= type "py") "python") 139 | ((string= type "rb") "ruby") 140 | ((string= type "js") "javascript") 141 | ((string= type "clj") "clojure") 142 | ((string= type "scm") "scheme") 143 | ((string= type "hs") "haskell") 144 | (t type)))) 145 | 146 | (defun compile-main-file (file filetype &key compile-to) 147 | (check-type file pathname) 148 | (let ((commands (cdr (assoc filetype *default-commands* :test 'string=)))) 149 | (unless commands 150 | (error "Unknown file type: ~A" filetype)) 151 | 152 | (let ((compilation-command (cdr (assoc "compile" commands :test 'string=)))) 153 | (when compilation-command 154 | (compile-code compilation-command 155 | file 156 | (and compile-to 157 | (normalize-pathname compile-to))))))) 158 | 159 | (defun make-execution-handler (file filetype &key compile-to) 160 | (check-type file pathname) 161 | (let ((commands (cdr (assoc filetype *default-commands* :test 'string=)))) 162 | (unless commands 163 | (error "Unknown file type: ~A" filetype)) 164 | (let ((execution-command (cdr (assoc "execute" commands :test 'string=)))) 165 | (lambda (input) 166 | (when execution-command 167 | (with-deadline 168 | (execute-code execution-command file input compile-to))))))) 169 | --------------------------------------------------------------------------------