diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..241f34e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI +on: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + if: ${{ github.repository == 'codewars/ocaml' }} + steps: + - uses: actions/checkout@v2 + - uses: docker/setup-buildx-action@v2 + + - name: Build image + uses: docker/build-push-action@v3 + with: + context: . + push: false + # Make the image available in next step + load: true + tags: ghcr.io/codewars/ocaml:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Batteries Example + run: bin/run examples/batteries + + - name: Run Base Example (with failing tests) + run: bin/run examples/base + + - name: Run Domainslib Example + run: bin/run examples/domainslib + + - name: Run Zarith Example + run: bin/run examples/zarith + + - name: Report Image Size + run: | + echo "## Image Size" >> $GITHUB_STEP_SUMMARY + docker image inspect --format '{{.Size}}' ghcr.io/codewars/ocaml:latest | numfmt --to=si --suffix=B >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile index c49803d..005abd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,48 +1,64 @@ -FROM buildpack-deps:bionic +FROM ubuntu:22.04 AS Builder -RUN set -ex; \ - useradd --create-home codewarrior; \ -# TODO Remove symlink in the next version - ln -s /home/codewarrior /workspace; - -ENV OPAMROOT=/opt/opam \ - OPAMCOLOR=never +ENV OPAMROOT=/opt/ocaml RUN set -ex; \ mkdir -p $OPAMROOT; \ + useradd --create-home codewarrior; \ chown codewarrior:codewarrior $OPAMROOT; \ apt-get update; \ apt-get install -y --no-install-recommends \ software-properties-common \ - m4 \ - rsync \ - aspcud \ - ; \ -# Needed for opam 2.0 - add-apt-repository -y ppa:avsm/ppa; \ + libgmp-dev \ + opam \ + ; + +USER codewarrior +ENV USER=codewarrior + +RUN set -ex; \ + opam init -y --shell-setup --disable-sandboxing --compiler=5.0.0; + +RUN set -ex; \ + opam install -y \ + 'batteries=3.6.0' \ + 'base=v0.15.1' \ + 'domainslib=0.5.0' \ + 'ocamlbuild=0.14.2' \ + 'ocamlfind=1.9.6' \ + 'ounit2=2.2.7' \ + 'zarith=1.12' \ + ; + +FROM ubuntu:22.04 + +RUN set -ex; \ apt-get update; \ apt-get install -y --no-install-recommends \ - opam \ + gcc \ + libc6-dev \ + libgmp-dev \ ; \ rm -rf /var/lib/apt/lists/*; -USER codewarrior -ENV USER=codewarrior \ - HOME=/home/codewarrior +COPY --from=builder \ + /opt/ocaml/5.0.0/bin/ocamlc.opt \ + /opt/ocaml/5.0.0/bin/ocamlopt.opt \ + /opt/ocaml/5.0.0/bin/ocamldep.opt \ + /opt/ocaml/5.0.0/bin/ocamlbuild \ + /opt/ocaml/5.0.0/bin/ocamlfind \ + /opt/ocaml/5.0.0/bin/ +COPY --from=builder \ + /opt/ocaml/5.0.0/lib/ /opt/ocaml/5.0.0/lib/ -# --disable-sandboxing is needed to do this in a container witout `--privileged` -RUN opam init -y --compiler=4.07.1 --disable-sandboxing - -ENV OPAM_SWITCH_PREFIX=/opt/opam/4.07.1 \ - CAML_LD_LIBRARY_PATH=/opt/opam/4.07.1/lib/stublibs \ - OCAML_TOPLEVEL_PATH=/opt/opam/4.07.1/lib/toplevel \ - PATH=/opt/opam/4.07.1/bin:$PATH +RUN set -ex; \ + useradd --create-home codewarrior; \ + mkdir -p /workspace; \ + chown -R codewarrior:codewarrior /workspace; -RUN opam install -y \ - 'ounit=2.0.8' \ - 'batteries=2.9.0' \ - 'core=v0.11.3' \ - ; +USER codewarrior +ENV USER=codewarrior \ + PATH=/opt/ocaml/5.0.0/bin:$PATH -COPY workspace/test.ml /workspace/test.ml -COPY workspace/_tags /workspace/_tags +COPY --chown=codewarrior:codewarrior workspace/. /workspace/ +WORKDIR /workspace diff --git a/DockerfileAlpine b/DockerfileAlpine new file mode 100644 index 0000000..ad32207 --- /dev/null +++ b/DockerfileAlpine @@ -0,0 +1,63 @@ +FROM alpine:3.17 AS builder + +ENV OPAMROOT=/opt/ocaml + +RUN set -ex; \ + mkdir -p $OPAMROOT; \ + adduser -D codewarrior; \ + chown -R codewarrior:codewarrior /opt/ocaml; \ + apk update; \ + apk add --virtual .build-deps \ + build-base \ + ocaml-compiler-libs \ + gmp-dev \ + opam \ + ; + +USER codewarrior +ENV USER=codewarrior + +RUN set -ex; \ + opam init -y --shell-setup --disable-sandboxing --compiler=5.0.0; + +RUN set -ex; \ + opam install -y \ + 'batteries=3.6.0' \ + 'base=v0.15.1' \ + 'domainslib=0.5.0' \ + 'ocamlbuild=0.14.2' \ + 'ocamlfind=1.9.6' \ + 'ounit2=2.2.7' \ + 'zarith=1.12' \ + ; + +FROM alpine:3.17 + +RUN set -ex; \ + apk add --no-cache \ + gcc \ + gmp-dev \ + musl-dev \ + ; + +COPY --from=builder \ + /opt/ocaml/5.0.0/bin/ocamlc.opt \ + /opt/ocaml/5.0.0/bin/ocamlopt.opt \ + /opt/ocaml/5.0.0/bin/ocamldep.opt \ + /opt/ocaml/5.0.0/bin/ocamlbuild \ + /opt/ocaml/5.0.0/bin/ocamlfind \ + /opt/ocaml/5.0.0/bin/ +COPY --from=builder \ + /opt/ocaml/5.0.0/lib/ /opt/ocaml/5.0.0/lib/ + +RUN set -ex; \ + adduser -D codewarrior; \ + mkdir /workspace; \ + chown -R codewarrior:codewarrior /workspace; + +USER codewarrior +ENV USER=codewarrior \ + PATH=/opt/ocaml/5.0.0/bin:$PATH + +COPY --chown=codewarrior:codewarrior workspace/. /workspace/ +WORKDIR /workspace \ No newline at end of file diff --git a/README.md b/README.md index 8074172..44c20c1 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,15 @@ Container image for OCaml used by CodeRunner. ## Usage ```bash -W=/workspace/ +W=/workspace # Create container -C=$(docker container create --rm -w $W ghcr.io/codewars/ocaml:latest \ - sh -c 'ocamlbuild -quiet -use-ocamlfind test.native && exec ./test.native') +BUILD="ocamlbuild -quiet -use-ocamlfind cwtest.native" +C=$(docker container create --rm -w $W ghcr.io/codewars/ocaml:latest sh -c "$BUILD && exec ./cwtest.native") -# Write solution and tests in workspace/fixture.ml -# Then copy it inside a container -docker container cp workspace/fixture.ml $C:$W/fixture.ml +# Copy solution and test files +docker container cp ${1:-examples/basic}/. $C:$W -# Run tests +# Start docker container start --attach $C ``` diff --git a/bin/alpine_build b/bin/alpine_build new file mode 100755 index 0000000..46aaf2f --- /dev/null +++ b/bin/alpine_build @@ -0,0 +1 @@ +docker build -f DockerfileAlpine -t ocaml:alpine . diff --git a/bin/alpine_run b/bin/alpine_run new file mode 100755 index 0000000..9916aa3 --- /dev/null +++ b/bin/alpine_run @@ -0,0 +1,2 @@ +export IMAGE=ocaml:alpine +exec "$(dirname "$0")/run" "$@" \ No newline at end of file diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..1115753 --- /dev/null +++ b/bin/build @@ -0,0 +1 @@ +docker build -t ghcr.io/codewars/ocaml:latest . \ No newline at end of file diff --git a/bin/run b/bin/run new file mode 100755 index 0000000..2b90fb7 --- /dev/null +++ b/bin/run @@ -0,0 +1,16 @@ +set -eu + +if [ -z "${IMAGE:+x}" ]; then + IMAGE=ghcr.io/codewars/ocaml:latest +fi + +W=/workspace +# Create container +BUILD="ocamlbuild -quiet -use-ocamlfind cwtest.native" +C=$(docker container create --rm -w $W $IMAGE sh -c "$BUILD && exec ./cwtest.native") + +# Copy files +docker container cp ${1:-examples/basic}/. $C:$W + +# Start +docker container start --attach $C \ No newline at end of file diff --git a/examples/base/preloaded.ml b/examples/base/preloaded.ml new file mode 100644 index 0000000..d310ac6 --- /dev/null +++ b/examples/base/preloaded.ml @@ -0,0 +1,3 @@ +open Base + +let print_int_list = Fn.compose Sexp.to_string (sexp_of_list sexp_of_int) \ No newline at end of file diff --git a/examples/base/solution.ml b/examples/base/solution.ml new file mode 100644 index 0000000..08a2b1c --- /dev/null +++ b/examples/base/solution.ml @@ -0,0 +1,3 @@ +open Base + +let cubes = List.map ~f:Int.(fun x -> x ** 3) \ No newline at end of file diff --git a/examples/base/tests.ml b/examples/base/tests.ml new file mode 100644 index 0000000..037e6f9 --- /dev/null +++ b/examples/base/tests.ml @@ -0,0 +1,15 @@ +open Base +open OUnit +open Preloaded + +let suite = [ + "cubes tests (passing)" >::: + [ + "Testing [1; 2; 3]" >:: (fun _ -> assert_equal [1; 8; 27] (Solution.cubes (List.range 1 4)) ~printer:print_int_list); + "Testing [5]" >:: (fun _ -> assert_equal [125] (Solution.cubes [5]) ~printer:print_int_list); + ]; + "cubes tests (failing)" >::: + [ + "Testing [1; 2; 3]" >:: (fun _ -> assert_equal [1; 8; 27] (Solution.cubes (List.range 1 5)) ~printer:print_int_list); + ]; + ] \ No newline at end of file diff --git a/examples/basic/preloaded.ml b/examples/basic/preloaded.ml new file mode 100644 index 0000000..e69de29 diff --git a/examples/basic/solution.ml b/examples/basic/solution.ml new file mode 100644 index 0000000..ea7dd3f --- /dev/null +++ b/examples/basic/solution.ml @@ -0,0 +1 @@ +let is_even n = n mod 2 = 0 \ No newline at end of file diff --git a/examples/basic/tests.ml b/examples/basic/tests.ml new file mode 100644 index 0000000..05c9e4d --- /dev/null +++ b/examples/basic/tests.ml @@ -0,0 +1,44 @@ +open Solution +open OUnit + +let suite = [ + "Top level test case" >:: (fun _ -> assert_equal false (is_even 1024)); + "Test Odd" >::: + [ + "Should return false for 1" >:: (fun _ -> assert_equal false (is_even 1)); + "Should return false for 7" >:: (fun _ -> assert_equal false (is_even 7)) + ]; + "Test even" >::: + [ + "Should return true for 100" >:: (fun _ -> assert_equal true (is_even 100)); + "Should return true for 42" >:: (fun _ -> assert_equal true (is_even 42)) + ]; + "Test edge cases" >::: + [ + "Test zero" >::: + [ + "Should return true for 0" >:: (fun _ -> assert_equal true (is_even 0)) + ]; + "Test -1" >::: + [ + "Should return false for -1" >:: (fun _ -> assert_equal false (is_even (-1))) + ] + ]; + "Unlabeled tests" >::: + [ + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100"); + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100"); + TestList [ + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100"); + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100"); + ] + + ]; + "Nested labels" >::: [ + "Outer label" >: ("Inner label" >: ("Tests with nested labels" >::: [ + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100" ~printer: string_of_bool); + TestCase (fun _ -> assert_equal false (is_even 100) ~msg:"Incorrect answer for n=100" ~printer: string_of_bool); + + ])) + ] + ] diff --git a/examples/batteries/preloaded.ml b/examples/batteries/preloaded.ml new file mode 100644 index 0000000..14fcab2 --- /dev/null +++ b/examples/batteries/preloaded.ml @@ -0,0 +1,4 @@ +open Batteries + +let print_int = IO.to_string Int.print +let print_int_list = IO.to_string (List.print Int.print) \ No newline at end of file diff --git a/examples/batteries/solution.ml b/examples/batteries/solution.ml new file mode 100644 index 0000000..e59fc78 --- /dev/null +++ b/examples/batteries/solution.ml @@ -0,0 +1,8 @@ +open Batteries + +let sum n = + (1 -- n) + |> Enum.map (fun i -> Num.num_of_int i |> Num.int_of_num) + |> Enum.sum + +let cubes = List.map Int.(fun x -> x ** 3) \ No newline at end of file diff --git a/examples/batteries/tests.ml b/examples/batteries/tests.ml new file mode 100644 index 0000000..4b07fed --- /dev/null +++ b/examples/batteries/tests.ml @@ -0,0 +1,16 @@ +open Batteries +open OUnit +open Preloaded + +let suite = [ + "sum tests" >::: + [ + "Testing 1" >:: (fun _ -> assert_equal 1 (Solution.sum 1) ~printer:print_int); + "Testing 9" >:: (fun _ -> assert_equal 45 (Solution.sum 9) ~printer:print_int); + ]; + "cubes tests" >::: + [ + "Testing [1; 2; 3]" >:: (fun _ -> assert_equal [1; 8; 27] (Solution.cubes ((1 -- 3) |> List.of_enum)) ~printer:print_int_list); + "Testing [5]" >:: (fun _ -> assert_equal [125] (Solution.cubes [5]) ~printer:print_int_list); + ]; + ] \ No newline at end of file diff --git a/examples/compile-error/solution.ml b/examples/compile-error/solution.ml new file mode 100644 index 0000000..ac637dd --- /dev/null +++ b/examples/compile-error/solution.ml @@ -0,0 +1,5 @@ +let sum n = + if n = 0 then + 0 + else + n + sum (n - 1) \ No newline at end of file diff --git a/examples/compile-error/tests.ml b/examples/compile-error/tests.ml new file mode 100644 index 0000000..772ae0a --- /dev/null +++ b/examples/compile-error/tests.ml @@ -0,0 +1,9 @@ +open OUnit + +let suite = [ + "sum tests" >::: + [ + "Testing 1" >:: (fun _ -> assert_equal 1 (Solution.sum 1) ~printer:string_of_int); + "Testing 9" >:: (fun _ -> assert_equal 45 (Solution.sum 9) ~printer:string_of_int); + ]; + ] \ No newline at end of file diff --git a/examples/domainslib/solution.ml b/examples/domainslib/solution.ml new file mode 100644 index 0000000..c45299f --- /dev/null +++ b/examples/domainslib/solution.ml @@ -0,0 +1,17 @@ +(* Copied from https://github.com/kayceesrk/ocaml5-tutorial *) +module T = Domainslib.Task + +let rec fib0 n = if n < 2 then 1 else fib0 (n - 1) + fib0 (n - 2) + +let rec fib_par pool n = + if n > 20 then begin + let a = T.async pool (fun _ -> fib_par pool (n - 1)) in + let b = T.async pool (fun _ -> fib_par pool (n - 2)) in + T.await pool a + T.await pool b + end else fib0 n + +let fib num_domains n = + let pool = T.setup_pool ~num_domains:(num_domains - 1) () in + let res = T.run pool (fun _ -> fib_par pool n) in + T.teardown_pool pool; + res \ No newline at end of file diff --git a/examples/domainslib/tests.ml b/examples/domainslib/tests.ml new file mode 100644 index 0000000..9273299 --- /dev/null +++ b/examples/domainslib/tests.ml @@ -0,0 +1,11 @@ +open OUnit + +let suite = [ + "fib tests" >::: + [ + "Testing 42 (1 domain)" >:: (fun _ -> assert_equal 433494437 (Solution.fib 1 42) ~printer:string_of_int); + "Testing 42 (2 domains)" >:: (fun _ -> assert_equal 433494437 (Solution.fib 2 42) ~printer:string_of_int); + "Testing 42 (3 domains)" >:: (fun _ -> assert_equal 433494437 (Solution.fib 3 42) ~printer:string_of_int); + "Testing 42 (4 domains)" >:: (fun _ -> assert_equal 433494437 (Solution.fib 4 42) ~printer:string_of_int); + ]; + ] \ No newline at end of file diff --git a/examples/unix/solution.ml b/examples/unix/solution.ml new file mode 100644 index 0000000..be8f2fa --- /dev/null +++ b/examples/unix/solution.ml @@ -0,0 +1 @@ +let getenv = Unix.environment \ No newline at end of file diff --git a/examples/unix/tests.ml b/examples/unix/tests.ml new file mode 100644 index 0000000..e4818e0 --- /dev/null +++ b/examples/unix/tests.ml @@ -0,0 +1,9 @@ +open OUnit +open Batteries + +let suite = [ + "getenv" >::: + [ + "Test" >:: (fun _ -> assert_equal [||] (Solution.getenv ()) ~printer:(IO.to_string @@ Array.print String.print_quoted)); + ]; + ] \ No newline at end of file diff --git a/examples/zarith/solution.ml b/examples/zarith/solution.ml new file mode 100644 index 0000000..1f54f94 --- /dev/null +++ b/examples/zarith/solution.ml @@ -0,0 +1,10 @@ +let fib_zarith n = + let rec fib n a b = + if n = 0 then a else fib (n - 1) b (Z.add a b) in + Z.rem (fib n Z.zero Z.one) (Z.of_int 1000007) + +let fib_num n = + let open Big_int in + let rec fib n a b = + if n = 0 then a else fib (n - 1) b (add_big_int a b) in + mod_big_int (fib n zero_big_int unit_big_int) (big_int_of_int 1000007) \ No newline at end of file diff --git a/examples/zarith/tests.ml b/examples/zarith/tests.ml new file mode 100644 index 0000000..3dd13e0 --- /dev/null +++ b/examples/zarith/tests.ml @@ -0,0 +1,16 @@ +open OUnit + +let suite = [ + "fib_zarith tests" >::: + [ + "Testing 42" >:: (fun _ -> assert_equal (Z.of_int 912427) (Solution.fib_zarith 42) ~cmp:Z.equal ~printer:Z.to_string); + "Testing 100000" >:: (fun _ -> assert_equal (Z.of_int 584523) (Solution.fib_zarith 100000) ~cmp:Z.equal ~printer:Z.to_string); + "Testing 1000000" >:: (fun _ -> assert_equal (Z.of_int 930254) (Solution.fib_zarith 1000000) ~cmp:Z.equal ~printer:Z.to_string); + ]; + "fib_num tests" >::: + [ + "Testing 42" >:: (fun _ -> assert_equal (Big_int.big_int_of_int 912427) (Solution.fib_num 42) ~cmp:Big_int.eq_big_int ~printer:Big_int.string_of_big_int); + "Testing 100000" >:: (fun _ -> assert_equal (Big_int.big_int_of_int 584523) (Solution.fib_num 100000) ~cmp:Big_int.eq_big_int ~printer:Big_int.string_of_big_int); + "Testing 1000000" >:: (fun _ -> assert_equal (Big_int.big_int_of_int 930254) (Solution.fib_num 1000000) ~cmp:Big_int.eq_big_int ~printer:Big_int.string_of_big_int); + ]; + ] \ No newline at end of file diff --git a/workspace/_tags b/workspace/_tags index c459396..aba10ca 100644 --- a/workspace/_tags +++ b/workspace/_tags @@ -1,2 +1,2 @@ - : package(oUnit), use_str - or : package(oUnit), package(batteries), package(core), thread +not : package(ounit2) +not : thread, package(batteries), package(base), package(domainslib), package(zarith) \ No newline at end of file diff --git a/workspace/test.ml b/workspace/cwtest.ml similarity index 94% rename from workspace/test.ml rename to workspace/cwtest.ml index e3dede9..f433715 100644 --- a/workspace/test.ml +++ b/workspace/cwtest.ml @@ -55,5 +55,4 @@ and run_test = function and run_tests tests = List.iter run_test tests -(* `solution` and `fixture` are concatenated to `fixture.ml` *) -let () = run_tests Fixture.Tests.suite +let () = run_tests Tests.suite diff --git a/workspace/cwtest.mli b/workspace/cwtest.mli new file mode 100644 index 0000000..ec8b0b5 --- /dev/null +++ b/workspace/cwtest.mli @@ -0,0 +1 @@ +(* Nothing is exported *) \ No newline at end of file diff --git a/workspace/fixture.ml b/workspace/fixture.ml deleted file mode 100644 index 2f0bf63..0000000 --- a/workspace/fixture.ml +++ /dev/null @@ -1,8 +0,0 @@ -(* TODO Avoid concatenating in the next version *) -let add x y = x + y;; - -module Tests = struct - open OUnit - let test1 test_ctxt = assert_equal 2 (add 1 1);; - let suite = ["1+1 is 2" >:: test1];; -end