diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..89c7d4f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Test + +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - learnocaml_image: "ocamlsf/learn-ocaml" + learnocaml_version: "0.12" + use_passwd: "false" + - learnocaml_image: "pfitaxel/learn-ocaml" + learnocaml_version: "oauth-moodle-dev" + use_passwd: "false" + - learnocaml_image: "pfitaxel/learn-ocaml" + learnocaml_version: "oauth-moodle-dev" + use_passwd: "true" + # at most 20 concurrent jobs per free account: + max-parallel: 4 + # don't cancel all in-progress jobs if one matrix job fails: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - name: Script + env: + EMACS_IMAGE_VERSION: "pfitaxel/emacs-learn-ocaml-client:oauth-moodle-dev" + LEARNOCAML_IMAGE: ${{ matrix.learnocaml_image }} + LEARNOCAML_VERSION: ${{ matrix.learnocaml_version }} + USE_PASSWD: ${{ matrix.use_passwd }} + run: | + make dist-tests || { ret=$?; make stop; exit $ret; } + make stop diff --git a/.gitignore b/.gitignore index 081bece..02330ac 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,10 @@ setup.log _opam/ # End of https://www.gitignore.io/api/emacs,ocaml + +### Misc ### + +tests/repo/server_config.json +confirm.txt +teacher.txt +*.pid diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f470dec..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -os: linux -dist: bionic -language: shell - -services: - - docker - -env: - global: - - EMACS_IMAGE="pfitaxel/emacs-learn-ocaml-client" - - LEARNOCAML_IMAGE="ocamlsf/learn-ocaml" - jobs: - - LEARNOCAML_VERSION="0.12" - - LEARNOCAML_VERSION="master" - -install: -- docker pull "$LEARNOCAML_IMAGE:$LEARNOCAML_VERSION" -- docker pull "$EMACS_IMAGE:$LEARNOCAML_VERSION" - -script: -- echo -e "${ANSI_YELLOW}Executing tests... ${ANSI_RESET}" && echo -en 'travis_fold:start:script\\r' -- ./test.sh -- echo -en 'travis_fold:end:script\\r' diff --git a/Makefile b/Makefile index 41d3260..b9a01e5 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,21 @@ ELC := $(ELFILE:.el=.elc) all: elc help: - @echo '$$ make elc # byte-compile $(ELFILE)' + @echo '$$ make elc # byte-compile $(ELFILE)' @echo '$$ make bump v=1.0.0 # Replace version strings with 1.0.0' + @echo '' + @echo 'All the following commands require sudo.' + @echo '' + @echo '$$ make back # Run a docker backend for interactive ERT tests' + @echo '$$ make back LEARNOCAML_IMAGE=ocamlsf/learn-ocaml LEARNOCAML_VERSION=0.12' + @echo '' + @echo '$$ make emacs # Run a dockerized emacs for ERT tests' + @echo '$$ make tests # Run dockerized ERT tests' + @echo '' + @echo '$$ make dist-tests # Alias-of: make back emacs tests' + @echo '$$ make dist-tests USE_PASSWD=true' + @echo '' + @echo '$$ make stop # Stop the docker backend and/or ERT frontend' bump: git diff -p --raw --exit-code || { echo >&2 "*** Please commit beforehand ***"; exit 1; } @@ -26,4 +39,19 @@ elc: $(ELC) clean: $(RM) $(ELC) -.PHONY: all help bump elc clean +back: + ./run_test_backend.sh + +emacs: + ./run_emacs_image.sh + +tests: + ./run_tests.sh + +dist-tests: back emacs tests + +stop: + -./stop_emacs_image.sh + ./stop_test_backend.sh + +.PHONY: all help bump elc clean back emacs tests dist-tests stop diff --git a/learn-ocaml.el b/learn-ocaml.el index 04268e5..1eb9000 100644 --- a/learn-ocaml.el +++ b/learn-ocaml.el @@ -26,6 +26,7 @@ (require 'cl-lib) (require 'browse-url) (require 'json) +(require 'subr-x) (require 'package) ; for #'learn-ocaml-upgrade-packages @@ -47,6 +48,9 @@ (defvar learn-ocaml-fail-noisely nil "Set `learn-ocaml-fail-noisely' to non-nil for `ert'-testing purposes.") +(defconst learn-ocaml-timeout 4 + "Time in s for `learn-ocaml-await-for' to wait after calling `make-process'.") + (defconst learn-ocaml-mode-version "1.0.0-git") (defconst learn-ocaml-command-name "learn-ocaml-client") @@ -59,6 +63,10 @@ (defvar learn-ocaml-temp-dir nil) +(defvar learn-ocaml-working-directory nil) + +(defvar learn-ocaml-use-passwd nil) + (defvar learn-ocaml-log-buffer nil) (defun learn-ocaml-log-buffer () @@ -97,6 +105,23 @@ Call `get-buffer-create' if need be, to ensure it is a live buffer." "Remove the trailing newline in STR." (replace-regexp-in-string "\n\\'" "" str)) +(defun learn-ocaml-replace-in-string (what with in) + "Replace WHAT by WITH in string IN." + (replace-regexp-in-string (regexp-quote what) with in nil 'literal)) + +(defun learn-ocaml-exercise-to-module-name (id) + (let* ((module-name (learn-ocaml-replace-in-string "_" "__" id)) + (module-name-bis (learn-ocaml-replace-in-string "-" "_0" module-name))) + module-name-bis)) + +(defun learn-ocaml-open-ml-file (id) + "Open ID in a new buffer and load prelude and prepare according to his name." + (find-file (concat id ".ml")) + (with-current-buffer (concat id ".ml") + (setq merlin-buffer-flags + (concat "-open Given_" + (learn-ocaml-exercise-to-module-name id))))) + (defun learn-ocaml-yes-or-no (message &optional dont-trap-quit) "Display MESSAGE in a yes-or-no popup. `\\[keyboard-quit]' is seen as nil, unless DONT-TRAP-QUIT is non-nil." @@ -108,6 +133,18 @@ Call `get-buffer-create' if need be, to ensure it is a live buffer." (funcall run) (quit nil))))) +(defun learn-ocaml-global-disable-mode () + "Disable learn-ocaml-mode' in ALL buffers." + (interactive "a") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (funcall 'learn-ocaml-mode -1)))) + +(defun learn-ocaml-get-progression-by-id (id json) + (if (cdr (assoc (intern id) json)) + (concat (number-to-string (cdr (assoc (intern id) json))) "%") + "N/A")) + (defun learn-ocaml-print-time-stamp () "Insert date/time in the buffer given by function `learn-ocaml-log-buffer'." (set-buffer (learn-ocaml-log-buffer)) @@ -118,6 +155,12 @@ Call `get-buffer-create' if need be, to ensure it is a live buffer." (current-time-string) " ---------------------\n"))) +(defun escape-secret (secret) + "Add escape to the secret when it's empty" + (if-let (secret-option (string= secret "")) + "\"\"" + secret)) + (defun learn-ocaml-file-writter-filter (file _proc string) "Write in FILE the given STRING. To be used as a `make-process' filter." @@ -170,6 +213,14 @@ Function added in the `kill-emacs-query-functions' hook." (ignore-errors (delete-directory (learn-ocaml-temp-dir))))) t) +(defun learn-ocaml-server-config (json) + "Set the global variable learn-ocaml-use-passwd according +to the boolean contained in the json returned by the client" + (if (string= (cdr (assoc 'use_passwd (json-read-from-string json))) + "true") + (setq learn-ocaml-use-passwd t) + (setq learn-ocaml-use-passwd nil))) + ;; ;; package.el shortcut ;; @@ -223,7 +274,20 @@ Call `learn-ocaml-display-exercise-list' if OPEN-EXO-LIST is non-nil." default-directory nil ""))) (make-directory dir t) - (setq default-directory dir)) + (setq default-directory dir) + (setq learn-ocaml-working-directory dir)) + (write-region "B .learn-ocaml" nil + (learn-ocaml-file-path learn-ocaml-working-directory ".merlin")) + (when (file-exists-p (concat learn-ocaml-working-directory "/.ocamlinit")) + (copy-file + (concat learn-ocaml-working-directory "/.ocamlinit") + (concat learn-ocaml-working-directory "/.ocamlinit~") t)) + (write-region (concat "#load .learn-ocaml/Given_" + (learn-ocaml-exercise-to-module-name "demo") + ".cmo;;\nopen Given_" + (learn-ocaml-exercise-to-module-name "demo") + ";;") nil + (learn-ocaml-file-path learn-ocaml-working-directory ".ocamlinit")) (if open-exo-list (learn-ocaml-display-exercise-list))) (cl-defun learn-ocaml-make-process-wrapper (&rest args &key command &allow-other-keys) @@ -254,29 +318,145 @@ add \"opam var bin\" (or another directory) in `exec-path'." (apply #'learn-ocaml-make-process-wrapper args) ; this could be a loop nil)))) +(defun learn-ocaml-await-for (fun-kk-body time &optional descr) + "Run FUN-KK-BODY, wait TIME s for a callback to be called, or failwith DESCR. +Return (t . \"stdout+stderr\") if exit code = 0; +Return (nil . \"stdout+stderr\") if exit code > 0." + (let ((wip t)) + (catch 'result + (let ((result + #'(lambda (res) + (if wip (throw 'result `(t . ,res)) + (message "learn-ocaml-await-for%s: Got result too late [%s]." + (if descr (concat "[" descr "]") "") res)))) + (failure + #'(lambda (res) + (if wip (throw 'result `(nil . ,res)) + (message "learn-ocaml-await-for%s: Got failure too late [%s]." + (if descr (concat "[" descr "]") "") res))))) + ;; Note that FUN-K-BODY is not wrapped by `with-timeout' + ;; as FUN-KK-BODY will typically be an async call to `make-process' + ;; and we just wait for the subprocess to terminate within TIME s. + (funcall fun-kk-body result failure) + (let ((end-time (+ (float time) (float-time)))) ;; start counting here + (while (< (float-time) end-time) + (accept-process-output nil 0.1))) + (setq wip nil) + (error + "learn-ocaml-await-for%s: didn't receive result after %ds." + (if descr (concat "[" descr "]") "") time))))) + +;; ;; TEST-CASE +;; (let ((buffer (generate-new-buffer "test"))) +;; (learn-ocaml-await-for +;; #'(lambda (result failure) +;; (make-process +;; :name "arg1" +;; :command '("sh" "-c" "echo output1; sleep 1s; echo error >&2; false") +;; :buffer buffer +;; :sentinel (apply-partially +;; #'learn-ocaml-error-handler-nosplit-catch +;; buffer +;; #'(lambda (s) (funcall result s)) +;; #'(lambda (s) (funcall failure s))))) +;; 2 ;; or 0 +;; "test")) + +(defun learn-ocaml-command-to-string-await-cmd (args) + "Run \"learn-ocaml-client ARGS\". +This is a `learn-ocaml-update-exec-path'-enhanced replacement for +(shell-command-to-string (combine-and-quote-strings + (cons \"learn-ocaml-client\" args))), relying on `learn-ocaml-await-for'. +Return (t . \"stdout+stderr\") if exit code = 0; +Return (nil . \"stdout+stderr\") if exit code > 0; +Raise (error \"learn-ocaml-await-for...\") if `learn-ocaml-timeout' exceeded." + ;; (learn-ocaml-print-time-stamp) ;; FIXME: enable? + (unless (and args (listp args)) + (error "ARGS must be a nonempty list (of strings)")) + (let ((buffer (generate-new-buffer (concat (car args) "-std-out")))) + (learn-ocaml-await-for + #'(lambda (result failure) + (learn-ocaml-make-process-wrapper + :name (car args) + :command (cons learn-ocaml-command-name args) + :buffer buffer + :sentinel (apply-partially + #'learn-ocaml-error-handler-nosplit-catch + buffer + #'(lambda (s) (funcall result s)) + #'(lambda (s) (funcall failure s))))) + learn-ocaml-timeout (car args)))) + +;; +;; Higher-order functions, sentinels of the make-process wrapper +;; +;; NOTE: if we want to get stdout+stderr output in one go, we need to +;; omit the :stderr argument of make-process. Otherwise, to get the +;; streams apart (e.g., for grade), the :stderr arg must be specified. + +;; FIXME: Move this docstring? +;; Summary of the workflow for sign-up/sign-in +;; - Choice (Sign-up/Login ?) +;; - Sign-up: +;; - Ask login, password, nickname, secret +;; - learn-ocaml-client init-user +;; - message-box stdout+stderr (e-mail sent) +;; - GoTo Choice +;; - Login: +;; - Ask login, password (TODO Check that OK if C-g) +;; - learn-ocaml-client init-user +;; - if exit code 0: callback “open exo list?” +;; - if exit code >0: +;; - message-box stdout+stderr (which will contain ERROR) +;; - GoTo Login + +;; FIXME: Add buffer-err argument, copied to (learn-ocaml-log-buffer) (defun learn-ocaml-error-handler (buffer callback proc string) "Get text from BUFFER and pass it to the CALLBACK. To be used as a `make-process' sentinel, using args PROC and STRING." -(let ((result (if (not buffer) + (let ((result (if (not buffer) "" - (set-buffer buffer) - (buffer-string)))) + (set-buffer buffer) + (buffer-string)))) (when buffer (kill-buffer buffer)) (if (or (string-equal string "finished\n") - (string-match "give-token" (process-name proc)) - (string-match "give-server" (process-name proc))) + (string-match "give-token" (process-name proc)) + (string-match "give-server" (process-name proc))) (funcall callback result) - (if (learn-ocaml-yes-or-no learn-ocaml-warning-message) - (progn (switch-to-buffer-other-window "*learn-ocaml-log*") - (goto-char (point-max)) - ;; Do this to cope with the addition of up-to 3 lines - ;; (... Process upload-demo stderr finished) - (recenter-top-bottom -3)) - (when learn-ocaml-fail-noisely - (with-current-buffer (learn-ocaml-log-buffer) - ;; Remark: the log will contain earlier, unrelated info... - (let ((log (buffer-string))) - (error "Process errored. Full log:\n%s" log)))))))) + (progn (set-buffer (learn-ocaml-log-buffer)) + (goto-char (point-max)) + (let ((message + ;; FIXME(Bug): this returns the whole buffer text! + (if (search-backward "[ERROR]" nil t 1) + (buffer-substring (point) (point-max)) ""))) + (cl-case (x-popup-dialog + t `(,message + ("Ok" . 1) + ("Check full learn-ocaml-log" . 2))) + (2 (switch-to-buffer-other-window "*learn-ocaml-log*"))))) + (when learn-ocaml-fail-noisely + (with-current-buffer (learn-ocaml-log-buffer) + ;; Remark: the log will contain earlier, unrelated info... + (let ((log (buffer-string))) + (error "Process errored. Full log:\n%s" log))))))) + +(defun learn-ocaml-error-handler-nosplit-catch (buffer callback-ok callback-err _proc string) + "Get text from BUFFER, pass it to CALLBACK-OK ($?=0) or CALLBACK-ERR. +To be used as a `make-process' sentinel, using args PROC and STRING." + (let ((result (if (not buffer) + "" + (set-buffer buffer) + (buffer-string)))) + (when buffer (kill-buffer buffer)) + (if (or (string-equal string "finished\n")) + (funcall callback-ok result) + (progn (set-buffer (learn-ocaml-log-buffer)) + (insert result) + (funcall callback-err result))))) + +;; +;; CLI constructors +;; (cl-defun learn-ocaml-command-constructor (&key command token server local id html dont-submit param1 param2) "Construct a shell command with `learn-ocaml-command-name' and options." @@ -289,10 +469,83 @@ To be used as a `make-process' sentinel, using args PROC and STRING." (list (list learn-ocaml-command-name command token-option server-option id-option html-option dont-submit-option local-option param1 param2))) (cl-remove-if-not 'stringp list))) +(cl-defun learn-ocaml-init-user-command-constructor (&key server login password nickname secret) + "Construct a shell command with `learn-ocaml-command-name' and options." + (let* ((nickname-option (when nickname nickname)) + (secret-option (when secret secret)) + (server-option (when server (concat "--server=" server))) + (list (list learn-ocaml-command-name "init-user" server-option login password nickname-option secret-option))) + (cl-remove-if-not 'stringp list))) + (defun learn-ocaml-client-version () "Run \"learn-ocaml-client --version\"." - (shell-command-to-string - (concat (shell-quote-argument learn-ocaml-command-name) " --version"))) + (string-trim + (cdr (learn-ocaml-command-to-string-await-cmd (list "--version"))))) + +(cl-defun learn-ocaml-client-sign-in-cmd (&key server login password callback-ok callback-err) + "Run \"learn-ocaml-client init-user\" with SERVER LOGIN PASSWORD to login an user." + (learn-ocaml-print-time-stamp) + (let ((buffer (generate-new-buffer "sign-in-std-out"))) + (learn-ocaml-make-process-wrapper + :name "init-user" + :command (learn-ocaml-init-user-command-constructor :server server + :login login + :password password) + :buffer buffer + :sentinel (apply-partially + #'learn-ocaml-error-handler-nosplit-catch + buffer + callback-ok + callback-err)))) + +(cl-defun learn-ocaml-client-sign-up-cmd (&key server login password nickname secret callback-ok callback-err) + "Run \"learn-ocaml-client init-user\" with SERVER LOGIN PASSWORD NICKNAME and SECRET to sign-up an user." + (learn-ocaml-print-time-stamp) + (let ((buffer (generate-new-buffer "sign-up-std-out"))) + (learn-ocaml-make-process-wrapper + :name "init-user" + :command (learn-ocaml-init-user-command-constructor :server server + :login login + :password password + :nickname nickname + :secret secret) + :buffer buffer + :sentinel (apply-partially + #'learn-ocaml-error-handler-nosplit-catch + buffer + callback-ok + callback-err)))) + +(defun learn-ocaml-client-config-cmd () + "Run \"learn-ocaml-client server-config\"." + (let* ((cmd "server-config") + (result (learn-ocaml-command-to-string-await-cmd (list cmd)))) + (if (car result) (cdr result) + ;; FIXME: Use learn-ocaml-log-buffer + (error "%s %s: failed with [%s]." learn-ocaml-command-name cmd (string-trim (cdr result)))))) + +(cl-defun learn-ocaml-init-server-cmd (&key server callback) + "Run \"learn-ocaml-client init\" with options." + (learn-ocaml-print-time-stamp) + (learn-ocaml-make-process-wrapper + :name "init-server" + :command (learn-ocaml-command-constructor + :server server + :command "init-server") + :stderr (learn-ocaml-log-buffer) + :sentinel (apply-partially + #'learn-ocaml-error-handler + nil + callback))) + +(defun learn-ocaml-client-exercise-score-cmd () + "Run \"learn-ocaml-client exercise-score\"." + (let* ((cmd "exercise-score") + (result (learn-ocaml-command-to-string-await-cmd (list cmd)))) + (if (car result) (json-read-from-string (cdr result)) + ;; FIXME: Use learn-ocaml-log-buffer + (error "%s %s: failed with [%s]." learn-ocaml-command-name cmd + (string-trim (cdr result)))))) (cl-defun learn-ocaml-init-cmd (&key token server nickname secret callback) "Run \"learn-ocaml-client init\" with options." @@ -302,9 +555,9 @@ To be used as a `make-process' sentinel, using args PROC and STRING." :command (learn-ocaml-command-constructor :token token :server server - :param1 nickname - :param2 secret - :command "init") + :param1 nickname + :param2 secret + :command "init") :stderr (learn-ocaml-log-buffer) :sentinel (apply-partially #'learn-ocaml-error-handler @@ -409,6 +662,7 @@ To be used as a `make-process' sentinel, using args PROC and STRING." (lambda (s) (funcall-interactively callback + (learn-ocaml--rstrip s))))))) (defun learn-ocaml-use-metadata-cmd (token server callback) @@ -455,15 +709,15 @@ Argument CALLBACK will receive the token." (learn-ocaml-make-process-wrapper :name "exercise-list" :command (learn-ocaml-command-constructor - :command "exercise-list") + :command "exercise-list") :stderr (learn-ocaml-log-buffer) :buffer buffer :sentinel (apply-partially - #'learn-ocaml-error-handler - buffer - (lambda (s) - (funcall-interactively - callback (json-read-from-string s))))))) + #'learn-ocaml-error-handler + buffer + (lambda (s) + (funcall-interactively + callback (json-read-from-string s))))))) (defun learn-ocaml-compute-questions-url (server id token) "Get subject url for SERVER, exercise ID and user TOKEN." @@ -483,7 +737,7 @@ Argument CALLBACK will receive the token." ;; very important if you don't do it you risk to open eww (setq browse-url-browser-function 'browse-url-default-browser) (browse-url (learn-ocaml-compute-questions-url server id token))))))) - + (defun learn-ocaml-show-metadata () "Display the token and server url in mini-buffer and `message-box'." (interactive) @@ -591,8 +845,8 @@ Argument SECRET may be needed by the server." :id learn-ocaml-exercise-id :file buffer-file-name :callback (lambda (html) - (setq browse-url-browser-function 'browse-url-default-browser) - (browse-url-of-file html)))) + (setq browse-url-browser-function 'browse-url-default-browser) + (browse-url-of-file html)))) ;; ;; exercise list display @@ -609,6 +863,17 @@ Argument SECRET may be needed by the server." :underline nil :weight bold :height 1.2))) "Face group titles.") +(defface learn-ocaml-nickname-face + '((t ( + :inherit variable-pitch + :foreground "LightSalmon1" + :underline nil :weight bold :height 1.3))) + "Face nickname.") + +(define-widget 'learn-ocaml-nickname 'lazy "" + :format "%{%t%}" + :sample-face 'learn-ocaml-nickname-face) + (define-widget 'learn-ocaml-group-title 'lazy "" :format "%{%t%}" :sample-face 'learn-ocaml-group-title-face) @@ -626,50 +891,53 @@ Argument SECRET may be needed by the server." :format "%{%t%}" :sample-face 'learn-ocaml-header-hint-face) -(defun learn-ocaml-print-exercise-info (indent tuple) +(defun learn-ocaml-print-exercise-info (indent tuple json-progression) "Render an exercise item with leading INDENT from the data in TUPLE." (let* ((id (elt tuple 0)) - (exo (elt tuple 1)) - (title (assoc-default 'title exo) ) - (short_description (assoc-default 'short_description exo)) - (stars (assoc-default 'stars exo))) + (exo (elt tuple 1)) + (title (assoc-default 'title exo) ) + (short_description (assoc-default 'short_description exo)) + (stars (assoc-default 'stars exo)) + (progression (learn-ocaml-get-progression-by-id id json-progression))) (widget-insert "\n") (widget-insert indent) (widget-create 'learn-ocaml-exercise-title - :tag title) + :tag title) (widget-insert "\n") (widget-insert (concat indent " ")) (widget-insert (if short_description short_description "No description available")) (widget-insert "\n") (widget-insert (concat indent " ")) (widget-insert (concat "Difficulty: " (number-to-string stars) "/4" - " id: " id)) + " progression: " + progression " id: " id )) (widget-insert "\n") (widget-insert (concat indent " ")) (widget-create 'learn-ocaml-button - :notify (lambda (&rest _ignore) - (learn-ocaml-show-questions id)) - "Browse subject") + :notify (lambda (&rest _ignore) + (learn-ocaml-show-questions id)) + "Browse subject") (widget-insert " ") (widget-create 'learn-ocaml-button - :notify (lambda (&rest _ignore) - (learn-ocaml-download-template id)) - "Get template") + :notify (lambda (&rest _ignore) + (learn-ocaml-download-template id)) + "Get template") (widget-insert " ") (widget-create 'learn-ocaml-button :notify (lambda (&rest _ignore) - (find-file (concat id ".ml"))) + (learn-ocaml-open-ml-file id)) "Open .ml") (widget-insert " ") (widget-create 'learn-ocaml-button - :notify (lambda (&rest _ignore) - (learn-ocaml-download-server-file id)) - "Get last saved version") + :notify (lambda (&rest _ignore) + (learn-ocaml-download-server-file id)) + "Get last saved version") (widget-insert "\n"))) (defun learn-ocaml-print-groups (indent json) "Render an exercise group with leading INDENT from the data in JSON." - (let ((head (car json)) + (let ((json-progression (learn-ocaml-client-exercise-score-cmd)) + (head (car json)) (queue (cdr json))) (if (eq 'groups head) (progn @@ -678,20 +946,20 @@ Argument SECRET may be needed by the server." (widget-create 'learn-ocaml-group-title :tag (concat indent - (cdr (car (cdr group))) - "\n")) + (cdr (car (cdr group))) + "\n")) (learn-ocaml-print-groups (concat indent " ") (car (cdr (cdr group))))) queue)) (seq-do (lambda (elt) (learn-ocaml-print-exercise-info - (concat indent " ") elt)) + (concat indent " ") elt json-progression)) queue) (widget-insert "\n")))) (defun learn-ocaml-display-exercise-list-aux (json) "Render the exercise list from the server-provided JSON." - (set-buffer (learn-ocaml-exo-list-buffer)) + (switch-to-buffer (learn-ocaml-exo-list-buffer)) (kill-all-local-variables) (let ((inhibit-read-only t)) (erase-buffer)) @@ -703,6 +971,14 @@ Argument SECRET may be needed by the server." 'learn-ocaml-header-hint :tag (make-string (length learn-ocaml-exo-list-doc) 45)) ; 45='-' (widget-insert "\n\n") + (widget-create + 'learn-ocaml-nickname + :tag (concat "Hello " (learn-ocaml-get-nickname) "! ")) + (widget-create 'learn-ocaml-button + :notify (lambda (&rest _ignore) + (learn-ocaml-set-nickname)) + "Change nickname") + (widget-insert "\n\n") (widget-insert "LearnOCaml ") (widget-create 'learn-ocaml-button :notify (lambda (&rest _ignore) @@ -727,8 +1003,7 @@ Argument SECRET may be needed by the server." (with-current-buffer learn-ocaml-exo-list-buffer ; just to be safe (read-only-mode 1)) ;; should be in the end, after (read-only-mode 1): - (learn-ocaml-mode) - (switch-to-buffer-other-window learn-ocaml-exo-list-buffer)) + (learn-ocaml-mode)) ;;;###autoload (defun learn-ocaml-display-exercise-list () @@ -749,7 +1024,7 @@ Argument SECRET may be needed by the server." Otherwise, call `learn-ocaml-create-token-cmd' if NEW-TOKEN-VALUE is nil. Otherwise, call `learn-ocaml-use-metadata-cmd'. Finally, run the CALLBACK. -Note: this function will be used by `learn-ocaml-on-load-aux'." +Note: this function will be used by `learn-ocaml-login-with-token'." (if new-server-value ;; without config file (learn-ocaml-init-cmd @@ -774,52 +1049,160 @@ Note: this function will be used by `learn-ocaml-on-load-aux'." nil callback)))))) -(defun learn-ocaml-on-load-aux (token server callback) +(defun learn-ocaml-login-possibly-with-passwd (server callback) + "Connect the user when learn-ocaml-use-passwd=true with an (email,passwd) or a token and continue with the no-arg CALLBACK." + (cl-case (x-popup-dialog + t `("Welcome to Learn OCaml mode for Emacs.\nWhat do you want to do?\n" + ("Login" . 1) + ("Sign-up" . 2) + ("Login with a legacy token" . 3))) + (1 (let* ((login_password (learn-ocaml-sign-in)) + (login (nth 0 login_password)) + (password (nth 1 login_password))) + (learn-ocaml-client-sign-in-cmd + :server server + :login login + :password password + :callback-ok (lambda(_) + (funcall callback)) + :callback-err (lambda(s) + (message-box s) + (learn-ocaml-login-possibly-with-passwd server callback))))) + (2 (let* ((infos (learn-ocaml-sign-up)) + (login (nth 0 infos)) + (password (nth 1 infos)) + (nickname (nth 2 infos)) + (secret (nth 3 infos))) + (learn-ocaml-client-sign-up-cmd + :server server + :login login + :password password + :nickname nickname + :secret secret + :callback-ok (lambda(s) + (message-box s) + (learn-ocaml-login-possibly-with-passwd server callback)) + :callback-err (lambda(s) + (message-box s) + (learn-ocaml-login-possibly-with-passwd server callback))))) + (3 (let ((token (read-string "Enter token: "))) + (learn-ocaml-use-metadata-cmd + token + nil + (lambda (_) + (message-box "Token saved."))))))) + +(defun learn-ocaml-sign-in () + "Ask interactively the e-mail and the password for the user to login" + (list (read-string "Enter e-mail: ") (read-passwd "Enter password: "))) + +(defun learn-ocaml-sign-up () + "Ask interactively the e-mail,password(with confirmation),nickname,secret" + (let* ((login (read-string "Enter e-mail: ")) + (pswd (read-passwd "Enter password: ")) + (pswd-conf (read-passwd "Enter password confirmation: ")) + (nickname (read-string "Enter nickname: ")) + (secret (read-string "Enter secret: "))) + (while (not (string= pswd pswd-conf)) + (setq pswd (read-passwd "Password are not the same. Enter password: ")) + (setq pswd-conf (read-passwd "Enter password confirmation: "))) + (list login pswd nickname secret))) + +(defun learn-ocaml-login-with-token (token new-server-value callback) "At load time: ensure a TOKEN and SERVER are set, then run CALLBACK. -If SERVER is \"\", interactively ask a server url. If TOKEN is \"\", interactively ask a token." - (let* ((new-server-value (when (or (not server) - (string-equal server "")) - (message-box "No server found. Please enter the server url.") - (read-string "Enter server URL: " "https://"))) - (rich-callback (lambda (_) - (funcall callback) - (learn-ocaml-show-metadata)))) + (let* ((rich-callback (lambda (_) + (funcall callback) + (learn-ocaml-show-metadata)))) (cl-destructuring-bind (token-phrase use-found-token use-another-token) - (if (or (not token) + (if (or (not token) (string-equal token "")) '("No token found" "Use found token" ("Use existing token" . 1)) `(,(concat "Token found: " token) ("Use found token" . 0) ("Use another token" . 1))) (cl-case (x-popup-dialog - t `(,(concat token-phrase "\n What do you want to do?\n") - ,use-found-token - ,use-another-token - ("Create new token" . 2))) - (0 (funcall rich-callback nil)) - - (1 (let ((token (read-string "Enter token: "))) - (learn-ocaml-init - :new-server-value new-server-value - :new-token-value token - :callback rich-callback))) - - (2 (let ((nickname (read-string "What nickname do you want to use for the token? ")) - (secret (read-string "What secret does the server require? "))) - (learn-ocaml-init - :new-server-value new-server-value - :nickname nickname - :secret secret - :callback rich-callback))))))) + t `(,(concat token-phrase "\n What do you want to do?\n") + ,use-found-token + ,use-another-token + ("Create new token" . 2))) + (0 (funcall rich-callback nil)) + (1 (let ((token (read-string "Enter token: "))) + (learn-ocaml-init + :new-server-value new-server-value + :new-token-value token + :callback rich-callback))) + (2 (let ((nickname (read-string "What nickname do you want to use for the token? ")) + (secret (read-string "What secret does the server require? "))) + (learn-ocaml-init + :new-server-value new-server-value + :nickname nickname + :secret secret + :callback rich-callback))))))) (defun learn-ocaml-on-load (callback) - "Call `learn-ocaml-on-load-aux' and CALLBACK when loading mode." + "Call `learn-ocaml-login-with-token' and CALLBACK when loading mode." (learn-ocaml-give-server-cmd (lambda (server) (learn-ocaml-give-token-cmd (lambda (token) - (learn-ocaml-on-load-aux token server callback)))))) + (let* ((new-server-value (if (or (not server) + (string-equal server "")) + (progn (message-box "No server found. Please enter the server url.") + (read-string "Enter server URL: " "https://")) + server))) + (progn (learn-ocaml-init-server-cmd :server new-server-value + :callback + (lambda(_) + (if (version-list-<= + (version-to-list (learn-ocaml-client-version)) (version-to-list "0.13")) + (progn (learn-ocaml-server-config (learn-ocaml-client-config-cmd)) + (if learn-ocaml-use-passwd + (learn-ocaml-login-possibly-with-passwd new-server-value callback) + (learn-ocaml-login-with-token token new-server-value callback))) + (learn-ocaml-login-with-token token new-server-value callback))))))))))) + +(defun learn-ocaml-logout () + "Logout the user from the server by removing the token from the file client.json" + (interactive) + (let* ((cmd "logout") + (result (learn-ocaml-command-to-string-await-cmd (list cmd)))) + (if (car result) + (progn (message-box "You have been successfully disconnected\n\n%s" + (cdr result)) + (learn-ocaml-global-disable-mode)) + ;; FIXME: Use learn-ocaml-log-buffer + (error "%s %s: failed with [%s]." learn-ocaml-command-name cmd + (string-trim (cdr result)))))) + +(defun learn-ocaml-deinit () + "Logout the user and forget the server by removing the file client.json" + ;; FIXME(Bug): + ;; $ learn-ocaml-client deinit + ;; should fail with exit code > 0 + ;; if Cannot remove ~/.config/learnocaml/client.json : no such file or directory. + (interactive) + (let* ((cmd "deinit") + (result (learn-ocaml-command-to-string-await-cmd (list cmd)))) + (if (car result) + (progn (message-box "You have been successfully disconnected\n\n%s" + (cdr result)) + (learn-ocaml-global-disable-mode)) + ;; FIXME: Use learn-ocaml-log-buffer + (error "%s %s: failed with [%s]." learn-ocaml-command-name cmd + (string-trim (cdr result)))))) + +(defun learn-ocaml-get-nickname () + "Get the nickname from the current user" + (string-trim (cdr (learn-ocaml-command-to-string-await-cmd (list "print-nickname"))))) + +(defun learn-ocaml-set-nickname () + "Ask a new nickname, set it and refresh the buffer." + (interactive) + (let* ((nickname (read-string "Enter your new nickname: "))) + (string-trim (cdr (learn-ocaml-command-to-string-await-cmd (list "set-nickname" nickname)))) + (kill-matching-buffers "^\*learn-ocaml.*\*" nil t) + (learn-ocaml-display-exercise-list))) ;; ;; menu definition @@ -857,12 +1240,14 @@ If TOKEN is \"\", interactively ask a token." ["Show exercise list" learn-ocaml-display-exercise-list] ["Download template" learn-ocaml-download-template] ["Download server version" learn-ocaml-download-server-file] - ["Grade" learn-ocaml-grade])) + ["Grade" learn-ocaml-grade] + "---" + ["Logout" learn-ocaml-logout] + ["Logout & Forget server" learn-ocaml-deinit])) ;; ;; id management ;; - (defun learn-ocaml-compute-exercise-id () "Store the exercise id of current buffer in `learn-ocaml-exercise-id'." (when buffer-file-name diff --git a/run_emacs_image.sh b/run_emacs_image.sh new file mode 100755 index 0000000..57f15bc --- /dev/null +++ b/run_emacs_image.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Note: this script need to be in the parent folder, not in tests/ +# because it runs a container with $PWD as bind-mount, and relies on +# both tests/learn-ocaml-tests.el and learn-ocaml.el + +# This file contains the Server Container ID (gen by ./run_test_backend.sh) +fcid="$PWD/learn-ocaml-server.pid" +# This file contains the Emacs Container ID (used by ./stop_emacs_image.sh) +feid="$PWD/learn-ocaml-emacs.pid" + +# Print $1 in green +green () { + echo -e "\\e[32m$1\\e[0m" +} + +# Print $1 in red +red () { + echo -e "\\e[31m$1\\e[0m" +} + +green "Beforehand: EMACS_IMAGE_VERSION=$EMACS_IMAGE_VERSION" +# Default emacs image +: "${EMACS_IMAGE_VERSION:=pfitaxel/emacs-learn-ocaml-client:oauth-moodle-dev}" +# Do "export EMACS_IMAGE_VERSION=…" before running the script to override +green "Henceforth: EMACS_IMAGE_VERSION=$EMACS_IMAGE_VERSION\\n" + +pull_ifneedbe () { + sudo docker pull "$EMACS_IMAGE_VERSION" + ret=$? + + if [ "$ret" -ne 0 ]; then + red "PROBLEM, 'sudo docker pull $EMACS_IMAGE_VERSION' failed with exit status $ret" + exit $ret + fi +} + +gen_emacs_cid () { + if [ -f "$feid" ]; then + red >&2 "Error: file '$feid' already exists: container is running?" + exit 1 + fi + echo "learn-ocaml-emacs-$$" >"$feid" + eid=$(<"$feid") +} + +read_cid () { + if [ -f "$fcid" ]; then + cid=$(<"$fcid") + else + red >&2 "Error: file '$fcid' does not exist." + exit 1 + fi +} + +stop_emacs () { + green "Stopping emacs..." + ( set -x && \ + rm -f "$feid" && \ + sudo docker logs "$eid"; \ + sudo docker stop "$eid" ) +} + +run_emacs () { + local oldopt; oldopt="$(set +o)"; set -x + + # Run the image in background + sudo docker run -d -i --init --rm --name="$eid" \ + -v "$PWD:/build" --network="container:$cid" \ + "$EMACS_IMAGE_VERSION" + ret=$? + + # hacky but working + sleep 2s + + set +vx; eval "$oldopt" # has to be after "ret=$?" + + if [ "$ret" -ne 0 ]; then + red "PROBLEM, 'sudo docker run -d ... $EMACS_IMAGE_VERSION' failed with exit status $ret" + stop_emacs + exit $ret + fi +} + +pull_ifneedbe +read_cid +gen_emacs_cid +run_emacs diff --git a/run_test_backend.sh b/run_test_backend.sh new file mode 100755 index 0000000..3fb1348 --- /dev/null +++ b/run_test_backend.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash + +# Note: this script need to be in the parent folder, not in tests/ +# because it runs a container with $PWD as bind-mount, and relies on +# both tests/learn-ocaml-tests.el and learn-ocaml.el + +# It reads the environment variable $USE_PASSWD (see below). + +# These files contain some useful/hidden info for ERT tests. +confirm="$PWD/confirm.txt" +teacher="$PWD/teacher.txt" + +# This file contains the Server Container ID (gen by ./run_test_backend.sh) +fcid="$PWD/learn-ocaml-server.pid" + +# Print $1 in green +green () { + echo -e "\\e[32m$1\\e[0m" +} + +# Print $1 in red +red () { + echo -e "\\e[31m$1\\e[0m" +} + +# Filter docker-logs +filter_confirm () { + stdbuf -o0 grep -e "$LEARNOCAML_BASE_URL/confirm/" || : +} + +green "Beforehand: LEARNOCAML_VERSION=$LEARNOCAML_VERSION" +# Default learn-ocaml version +: "${LEARNOCAML_VERSION:=oauth-moodle-dev}" +# Do "export LEARNOCAML_VERSION=…" before running the script to override +green "Henceforth: LEARNOCAML_VERSION=$LEARNOCAML_VERSION\\n" + +green "Beforehand: LEARNOCAML_IMAGE=$LEARNOCAML_IMAGE" +# Default learn-ocaml image +: "${LEARNOCAML_IMAGE:=pfitaxel/learn-ocaml}" +# Do "export LEARNOCAML_IMAGE=…" before running the script to override +green "Henceforth: LEARNOCAML_IMAGE=$LEARNOCAML_IMAGE\\n" + +green "Beforehand: LEARNOCAML_BASE_URL=$LEARNOCAML_BASE_URL" +# Default emacs image +: "${LEARNOCAML_BASE_URL:=http://localhost:8080}" +# Do "export LEARNOCAML_BASE_URL=…" before running the script to override +green "Henceforth: LEARNOCAML_BASE_URL=$LEARNOCAML_BASE_URL\\n" + +green "Beforehand: USE_PASSWD=$USE_PASSWD" +# Default mode +: "${USE_PASSWD:=false}" +# Do "export USE_PASSWD=…" before running the script to override +green "Henceforth: USE_PASSWD=$USE_PASSWD\\n" + +pull_ifneedbe () { + sudo docker pull "$LEARNOCAML_IMAGE:$LEARNOCAML_VERSION" + ret=$? + + if [ "$ret" -ne 0 ]; then + red "PROBLEM, 'sudo docker pull $LEARNOCAML_IMAGE:$LEARNOCAML_VERSION' failed with exit status $ret" + exit $ret + fi +} + +############################################################################### +### BACKUP OF OLD CODE ### +## Assuming /dev/fd/3 is free +# +#function trap_exit() { +# local fi="$1" +# exec 3>&- # close descriptor +# set -x; rm "$fi" || true +#} +# +#filter_confirm () { grep -e "$LEARNOCAML_BASE_URL/confirm/" || :; } +# +#fi="$PWD/logger" +#out="$PWD/confirm.txt" +# +#if [[ ! -p "$fi" ]]; then +# mkfifo "$fi" +#else +# echo >&2 "Info: fifo '$fi' already exists." +#fi +# +#if [[ -r "$out" ]]; then +# echo >&2 "Info: file '$out' already exists, saving..." +# cp --backup=t -fv "$out" "$out" +#fi +#>"$out" # erase file "$out" +# +#cat "$fi" | filter_confirm >>"$out" & +# +#exec 3>"$fi" # keep the input of $fi open +# +#trap 'trap_exit "$fi"' EXIT +# +## Run `cat "$fi"` or `tail --follow "$fi"` to read from the named pipe +## Run `echo ok >"$fi" &` or `echo ok >&3 &` to write in the named pipe +# +############################################################################### + +wait_for_it () { + local url="$1" + local seconds="$2" + shift 2 # "$@" => optional command to be run in case of success + local elapsed=0 + green "waiting $seconds seconds for $url\\n" + while :; do + curl -fsS "$url" >/dev/null 2>/dev/null && break + [ "$elapsed" -ge "$seconds" ] && + { red "timeout occurred after waiting $seconds seconds for $url\\n"; + return 124; } + elapsed=$((elapsed + 1)) + sleep 1s + done + green "$url available after $elapsed seconds" + if [ $# -gt 0 ]; then + ( set -x; "$@" ) + fi + return 0 +} + +gen_cid () { + if [ -f "$fcid" ]; then + red >&2 "Error: file '$fcid' already exists: container is running?" + exit 1 + fi + echo "learn-ocaml-server-$$" >"$fcid" + cid=$(<"$fcid") +} + +stop_server () { + green "Stopping server..." + ( set -x && \ + rm -f "$fcid" && \ + sudo docker logs "$cid"; \ + sudo docker stop "$cid" ) +} + +run_server () { + local oldopt; oldopt="$(set +o)"; set -x + + if [ "$USE_PASSWD" = "true" ]; then + cp -f "$PWD/tests/use_passwd.json" "$PWD/tests/repo/server_config.json" + else + # TODO: Add a secret + rm -f "$PWD/tests/repo/server_config.json" + fi + + # Don't use the "-d" option + sudo docker run --name="$cid" --rm -p 8080:8080 \ + -e LEARNOCAML_BASE_URL="$LEARNOCAML_BASE_URL" \ + -v "$PWD/tests/repo:/repository" \ + "$LEARNOCAML_IMAGE:$LEARNOCAML_VERSION" build serve 2>&1 | \ + filter_confirm >"$confirm" & + + set +vx; eval "$oldopt" + + # Increase this timeout if ever one sub-repo build would last > 10s + build_timeout=10 + + if ! wait_for_it "http://localhost:8080/version" "$build_timeout" sleep 1s; then + red >&2 "PROBLEM, 'sudo docker run ... $LEARNOCAML_IMAGE:$LEARNOCAML_VERSION' does not run." + stop_server + exit 1 + fi +} + +read_teacher () { + echo 'Teacher token:' + sudo docker logs "$cid" | \ + grep -e 'Initial teacher token created:' | \ + sed -e 's/^.*: //' | tr -d '[:space:]' | tee "$teacher" + echo +} + +pull_ifneedbe +gen_cid +run_server +read_teacher diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..e710f5a --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +# Note: this script need to be in the parent folder, not in tests/ +# because it runs a container with $PWD as bind-mount, and relies on +# both tests/learn-ocaml-tests.el and learn-ocaml.el + +# It reads the environment variable $USE_PASSWD (see below). + +# These files contain some useful/hidden info for ERT tests. +confirm="$PWD/confirm.txt" +teacher="$PWD/teacher.txt" +# TODO Use them. + +# This file contains the Server Container ID (gen by ./run_test_backend.sh) +fcid="$PWD/learn-ocaml-server.pid" +# This file contains the Emacs Container ID (gen by ./stop_emacs_image.sh) +feid="$PWD/learn-ocaml-emacs.pid" + +# Print $1 in green +green () { + echo -e "\\e[32m$1\\e[0m" +} + +# Print $1 in red +red () { + echo -e "\\e[31m$1\\e[0m" +} + +green "Beforehand: USE_PASSWD=$USE_PASSWD" +# Default mode +: "${USE_PASSWD:=false}" +# Do "export USE_PASSWD=…" before running the script to override +green "Henceforth: USE_PASSWD=$USE_PASSWD\\n" + +read_cid () { + if [ -f "$fcid" ]; then + cid=$(<"$fcid") + else + red >&2 "Error: file '$fcid' does not exist." + exit 1 + fi +} + +read_eid () { + if [ -f "$feid" ]; then + eid=$(<"$feid") + else + red >&2 "Error: file '$feid' does not exist." + exit 1 + fi +} + +assert () { + if [ $# -ne 1 ]; then + red "ERROR, assert expects a single arg (the code to run)" + exit 1 + fi + sudo docker exec -i "$eid" /bin/sh -c " +set -ex +$1 +" + ret=$? + if [ "$ret" -ne 0 ]; then + red "FAILURE, this shell command returned exit status $ret: +\$ sudo docker exec -i $eid /bin/sh -c '$1'\\n" + exit $ret + fi +} + +read_cid +read_eid + +assert "emacs --version" +assert "emacs --batch --eval '(pp (+ 2 2))'" +echo + +assert "learn-ocaml-client --version" + +if [ "$USE_PASSWD" = "true" ]; then + # TODO: Refactor this to run the init command from ERT's fixture + init='learn-ocaml-client init-user -s http://localhost:8080 foo@example.com OCaml123_ Foo ""' + selector="" +else + init='learn-ocaml-client init --server=http://localhost:8080 Foo ""' + selector="learn-ocaml-test-skip-use-passwd" +fi + +assert " +cd /build/tests +$init +emacs --batch -l ert -l init-tests.el -l /build/learn-ocaml.el \ + -l learn-ocaml-tests.el --eval '(ert-run-tests-batch-and-exit $selector)' +" diff --git a/stop_emacs_image.sh b/stop_emacs_image.sh new file mode 100755 index 0000000..f99a36e --- /dev/null +++ b/stop_emacs_image.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Note: this script need to be in the parent folder, not in tests/ +# because it runs a container with $PWD as bind-mount, and relies on +# both tests/learn-ocaml-tests.el and learn-ocaml.el + +# Print $1 in green +green () { + echo -e "\\e[32m$1\\e[0m" +} + +# Print $1 in red +red () { + echo -e "\\e[31m$1\\e[0m" +} + +read_eid () { + # File containing the Emacs Container ID + feid="$PWD/learn-ocaml-emacs.pid" + if [ -f "$feid" ]; then + eid=$(<"$feid") + else + red >&2 "Error: file '$feid' does not exist."; exit 1 + fi +} + + +stop_emacs () { + green "Stopping emacs..." + ( set -x && \ + rm -f "$feid" && \ + sudo docker stop "$eid" ) +} + +read_eid +stop_emacs diff --git a/stop_test_backend.sh b/stop_test_backend.sh new file mode 100755 index 0000000..67fd2e4 --- /dev/null +++ b/stop_test_backend.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Note: this script need to be in the parent folder, not in tests/ +# because it runs a container with $PWD as bind-mount, and relies on +# both tests/learn-ocaml-tests.el and learn-ocaml.el + +# Print $1 in green +green () { + echo -e "\\e[32m$1\\e[0m" +} + +# Print $1 in red +red () { + echo -e "\\e[31m$1\\e[0m" +} + +read_cid () { + # File containing the Server Container ID + fcid="$PWD/learn-ocaml-server.pid" + if [ -f "$fcid" ]; then + cid=$(<"$fcid") + else + red >&2 "Error: file '$fcid' does not exist." + exit 1 + fi +} + + +stop_server () { + green "Stopping server..." + ( set -x && \ + rm -f "$fcid" && \ + sudo docker stop "$cid" ) +} + +read_cid +stop_server diff --git a/test.sh b/test.sh deleted file mode 100755 index e09a3d4..0000000 --- a/test.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/bash - -# Note: this script need to be in the parent folder, not in tests/ -# because it runs a container with $PWD as bind-mount, and relies on -# both tests/learn-ocaml-tests.el and learn-ocaml.el - -# Print $1 in green -green () { - echo -e "\e[32m$1\e[0m" -} - -# Print $1 in red -red () { - echo -e "\e[31m$1\e[0m" -} - -green "Beforehand: LEARNOCAML_VERSION=$LEARNOCAML_VERSION" -# Default learn-ocaml version -: ${LEARNOCAML_VERSION:=dev} -# Do "export LEARNOCAML_VERSION=…" before running test.sh to override -green "Henceforth: LEARNOCAML_VERSION=$LEARNOCAML_VERSION\n" - -green "Beforehand: LEARNOCAML_IMAGE=$LEARNOCAML_IMAGE" -# Default learn-ocaml image -: ${LEARNOCAML_IMAGE:=ocamlsf/learn-ocaml} -# Do "export LEARNOCAML_IMAGE=…" before running test.sh to override -green "Henceforth: LEARNOCAML_IMAGE=$LEARNOCAML_IMAGE\n" - -green "Beforehand: EMACS_IMAGE=$EMACS_IMAGE" -# Default emacs image -: ${EMACS_IMAGE:=pfitaxel/emacs-learn-ocaml-client} -# Do "export EMACS_IMAGE=…" before running test.sh to override -green "Henceforth: EMACS_IMAGE=$EMACS_IMAGE\n" - -SERVER_NAME="learn-ocaml-server" -EMACS_NAME="emacs-client" - -# run a server in a docker container -run_server () { - local oldopt="$(set +o)"; set -x - # Run the server in background - sudo docker run -d --rm --name="$SERVER_NAME" \ - -v "$PWD/tests/repo:/repository" \ - "$LEARNOCAML_IMAGE:$LEARNOCAML_VERSION" - ret=$? - set +vx; eval "$oldopt" # has to be after "ret=$?" - if [ "$ret" -ne 0 ]; then - red "PROBLEM, 'sudo docker run -d ... $LEARNOCAML_IMAGE:$LEARNOCAML_VERSION' failed with exit status $ret" - exit $ret - fi - - # Wait for the server to be initialized - sleep 2 - - if [ -z "$(sudo docker ps -q)" ]; then - red "PROBLEM, server is not running" - exit 1 - fi -} - -stop_server () { - green "Stopping server..." - ( set -x && \ - # sudo docker logs "$SERVER_NAME" - sudo docker stop "$SERVER_NAME" ) -} - -# run an emacs in a docker container -run_emacs () { - local oldopt="$(set +o)"; set -x - # Run the server in background - sudo docker run -d -i --init --rm --name="$EMACS_NAME" \ - -v "$PWD:/build" --network="container:$SERVER_NAME" \ - "$EMACS_IMAGE:$LEARNOCAML_VERSION" - ret=$? - set +vx; eval "$oldopt" # has to be after "ret=$?" - if [ "$ret" -ne 0 ]; then - red "PROBLEM, 'sudo docker run -d ... $EMACS_IMAGE:$LEARNOCAML_VERSION' failed with exit status $ret" - exit $ret - fi -} - -stop_emacs () { - green "Stopping emacs..." - ( set -x && \ - sudo docker stop "$EMACS_NAME" ) -} - -assert () { - if [ $# -ne 1 ]; then - red "ERROR, assert expects a single arg (the code to run)" - exit 1 - fi - sudo docker exec -i "$EMACS_NAME" /bin/sh -c " -set -ex -$1 -" - ret=$? - if [ "$ret" -ne 0 ]; then - red "FAILURE, this shell command returned exit status $ret: -\$ sudo docker exec -i $EMACS_NAME /bin/sh -c '$1'\n" - stop_emacs - stop_server - exit $ret - fi -} - -############################################################################### - -run_server -run_emacs - -assert "emacs --version" - -assert "emacs --batch --eval '(pp (+ 2 2))'" - -echo - -assert "learn-ocaml-client --version" - -assert " -cd /build/tests -learn-ocaml-client init --server=http://localhost:8080 test test -emacs --batch -l ert -l init-tests.el -l /build/learn-ocaml.el -l learn-ocaml-tests.el -f ert-run-tests-batch-and-exit -" - -stop_emacs -stop_server diff --git a/tests/README.md b/tests/README.md index 1f789bb..9f74988 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,15 @@ # How to run tests -## Using the Travis CI wrapper +## Using only Docker containers +* (Useful to preserve `~/.config/learnocaml/client.json` on the host.) * Install Docker -* Run `cd .. && ./test.sh` +* Run `cd ..; make dist-tests; make stop` ## Using ert within emacs * Install Docker +* Run `cd ..; make back` * Install `learn-ocaml` using OPAM (`make && make opaminstall`) * Add `learn-ocaml-client` in the `PATH`, e.g. run in a terminal: @@ -20,12 +22,7 @@ * Run then: ```bash -cd .../learn-ocaml.el -export LOVERSION=dev -docker pull ocamlsf/learn-ocaml:$LOVERSION -docker run --name=server -d --rm -p 8080:8080 \ - -v "$PWD/tests/repo:/repository" ocamlsf/learn-ocaml:$LOVERSION build serve -learn-ocaml-client init -s http://localhost:8080 test test +# learn-ocaml-client init -s http://localhost:8080 test test emacs tests/learn-ocaml-tests.el & ``` @@ -38,8 +35,99 @@ emacs tests/learn-ocaml-tests.el & ## Note to learn-ocaml.el's CI maintainers +* (**WARNING**: Needs documentation update.) + To test learn-ocaml.el w.r.t. another version of learn-ocaml-client: * Push a new branch in , * Wait for the image in , * Add a new job line (`LEARNOCAML_VERSION="0.xx"`) in this repo's `.travis.yml`. + +# Description of the test-suite + +## Unit tests + +* `a13_learn-ocaml-file-path` (`ert-deftest`) + * Let ex = `/bin/bash` + * Let dir = `file-name-directory(ex)` + * Let file = `file-name-nondirectory(ex)` + * Check ex == `learn-ocaml-file-path(directory-file-name(dir),file)` + * Check ex == `learn-ocaml-file-path(dir,file)` + * Check ex == `learn-ocaml-file-path("/dummy",file)` + +## Integration tests (`ert-deftest-async`) + +**BEWARE** that some tests do `rm -f ~/.config/learnocaml/client.json` + +*Note* that the tests use `http://localhost:8080` as server URL. + +* `1_learn-ocaml-server-management-test` + * Set server (`learn-ocaml-use-metadata-cmd`) + * Read server (`learn-ocaml-give-server-cmd`) +* `2_learn-ocaml-token-management-test` + * Create token from nickname, secret (`learn-ocaml-create-token-cmd`) + * **TODO** Change nickname(test→Foo) + * **TODO** Add secret to `server_config.json` + * Set token (`learn-ocaml-use-metadata-cmd`) + * Read token (`learn-ocaml-give-token-cmd`) +* `3_learn-ocaml-grade-test` + * `rm -f /tmp/learn-ocaml-mode$XXXXXX/demo-results.html` + * Grade `tests/to_grade.ml` for `demo` (`learn-ocaml-grade-file-cmd`) + * `grep "Exercise complete" /tmp/learn-ocaml-mode$XXXXXX/demo-results.html` (`should return 0`) + * `rm -f /tmp/learn-ocaml-mode$XXXXXX/demo-results.html` +* `4_learn-ocaml-download-server-file-test` + * `rm -f /tmp/learn-ocaml-mode$XXXXXX/demo.ml` + * Download `demo.ml` (`learn-ocaml-download-server-file-cmd`) + * `cat /tmp/learn-ocaml-mode$XXXXXX/demo.ml` (`should return 0`) + * **TODO** Improve/Split this test, using `diff` or so +* `5_learn-ocaml-download-template-test` + * `rm -f /tmp/learn-ocaml-mode$XXXXXX/demo.ml` + * Download template `demo.ml` (`learn-ocaml-download-template-cmd`) + * `diff /tmp/learn-ocaml-mode$XXXXXX/demo.ml ./tests/template_demo.ml` (`should return 0`) + * `rm -f /tmp/learn-ocaml-mode$XXXXXX/demo.ml` +* `6_learn-ocaml-give-exercise-list-test` + * Get exercise list (`learn-ocaml-give-exercise-list-cmd`) + * Read `./tests/exercise_list.json` + * They should be equal + * **TODO** Extend this test, using more subdirs? +* `7_learn-ocaml-compute-questions-url-test` + * Read server, token (`learn-ocaml-give-server-cmd`, `learn-ocaml-give-token-cmd`) + * Get description URL of `demo` (`learn-ocaml-compute-questions-url`) + * `curl -fsS $URL` + * Read `./tests/expected_description.html` + * They should be equal +* `8_learn-ocaml-init-another-token` + * Create token from nickname, secret (`learn-ocaml-create-token-cmd`) + * **TODO** Change nickname(test→Foo) + * **TODO** Add secret to `server_config.json` + * Test `learn-ocaml-init` {used by `learn-ocaml-login-with-token`} with new token + * Read new token (`learn-ocaml-give-token-cmd`) + * They should be equal +* `9_learn-ocaml-init-create-token` + * Read token (`learn-ocaml-give-token-cmd`) + * Test `learn-ocaml-init` {used by `learn-ocaml-login-with-token`} with no token + * which will call `learn-ocaml-create-token-cmd` + * Read new token (`learn-ocaml-give-token-cmd`) + * It should be different +* `a10_learn-ocaml-on-load-test-another-token-no-config` + * Read token (`learn-ocaml-give-token-cmd`) + * `rm -f ~/.config/learnocaml/client.json` + * Test `learn-ocaml-init` {used by `learn-ocaml-login-with-token`} with URL, initial token + * Read token (`learn-ocaml-give-token-cmd`) + * They should be equal +* `a11_learn-ocaml-on-load-test-create-token-no-config` + * Read token (`learn-ocaml-give-token-cmd`) + * `rm -f ~/.config/learnocaml/client.json` + * Test `learn-ocaml-init` {used by `learn-ocaml-login-with-token`} with URL, nickname, secret + * **TODO** Change nickname(test→Foo) + * **TODO** Add secret to `server_config.json` + * Read token (`learn-ocaml-give-token-cmd`) + * **FIXME** It should be different +* `a12_learn-ocaml-test-sign-up` + * Test `learn-ocaml-client-sign-up-cmd` {used by `learn-ocaml-login-possibly-with-passwd`} with URL, email, passwd, nickname, empty secret + * **TODO** Change nickname(Test→Foo) + * **TODO** Add secret ≠ "" + * Get stdout+stderr + * Compare with hardcoded string + * **TODO** Improve this text, using `string-match-p` i/o `string-equa` +* **TODO** Test `learn-ocaml-login-possibly-with-passwd` diff --git a/tests/learn-ocaml-tests.el b/tests/learn-ocaml-tests.el index f31ffd3..b8d30db 100644 --- a/tests/learn-ocaml-tests.el +++ b/tests/learn-ocaml-tests.el @@ -26,6 +26,12 @@ (require 'ert-async) ;(setq ert-async-timeout 2) +;; NOTE: This symbol list gather tests specific to 'use_passwd: true' +(setq learn-ocaml-test-use-passwd-list + '(a12_learn-ocaml-test-sign-up 2_learn-ocaml-token-management-test)) + +(setq learn-ocaml-test-skip-use-passwd + `(not (member ,@learn-ocaml-test-use-passwd-list))) ;; WARNING: several tests delete the ./demo.ml and client.json files: (setq learn-ocaml-test-client-file "~/.config/learnocaml/client.json") @@ -67,97 +73,97 @@ (ert-deftest-async 1_learn-ocaml-server-management-test (done) (let ((tests (lambda (callback) - (learn-ocaml-use-metadata-cmd - nil - learn-ocaml-test-url - (lambda (_) - (learn-ocaml-give-server-cmd - (lambda (given-server) - (should (string-equal - learn-ocaml-test-url - given-server)) - (funcall callback)))))))) + (learn-ocaml-use-metadata-cmd + nil + learn-ocaml-test-url + (lambda (_) + (learn-ocaml-give-server-cmd + (lambda (given-server) + (should (string-equal + learn-ocaml-test-url + given-server)) + (funcall callback)))))))) (funcall tests done))) (ert-deftest-async 2_learn-ocaml-token-management-test (done) (let ((tests (lambda (callback) - (learn-ocaml-create-token-cmd - "test" - "test" - (lambda (token) - (learn-ocaml-use-metadata-cmd - token - nil - (lambda (_) - (learn-ocaml-give-token-cmd - (lambda (given_token) - (should - (string-equal - given_token - token )) - (funcall callback)))))))))) + (learn-ocaml-create-token-cmd + "test" + "test" + (lambda (token) + (learn-ocaml-use-metadata-cmd + token + nil + (lambda (_) + (learn-ocaml-give-token-cmd + (lambda (given_token) + (should + (string-equal + given_token + token )) + (funcall callback)))))))))) (funcall tests done))) (ert-deftest-async 3_learn-ocaml-grade-test (done) (learn-ocaml-test-remove-temp-file "demo") (let ((test (lambda(callback) - (learn-ocaml-grade-file-cmd - :id "demo" - :file learn-ocaml-test-tograde-file - :callback (lambda (_) - (should (= (shell-command - (concat - "cat " - (learn-ocaml-temp-html-file "demo") - " | grep \"Exercise complete\"") - ) - 0)) + (learn-ocaml-grade-file-cmd + :id "demo" + :file learn-ocaml-test-tograde-file + :callback (lambda (_) + (should (= (shell-command + (concat + "cat " + (learn-ocaml-temp-html-file "demo") + " | grep \"Exercise complete\"") + ) + 0)) (learn-ocaml-test-remove-temp-file "demo") - (funcall callback)))))) + (funcall callback)))))) (funcall test done))) (ert-deftest-async 4_learn-ocaml-download-server-file-test (done) (learn-ocaml-test-remove-demo-file) (let ((test (lambda(callback) - (learn-ocaml-download-server-file-cmd + (learn-ocaml-download-server-file-cmd :id "demo" :directory learn-ocaml-fixture-directory - :callback (lambda (s) - (should (= 0 (shell-command - (concat "cat " + :callback (lambda (s) + (should (= 0 (shell-command + (concat "cat " learn-ocaml-test-demo-file)))) - (learn-ocaml-test-remove-demo-file t) - (funcall callback)))))) + (learn-ocaml-test-remove-demo-file t) + (funcall callback)))))) (funcall test done))) (ert-deftest-async 5_learn-ocaml-download-template-test (done) (learn-ocaml-test-remove-demo-file) (let ((test (lambda (callback) - (learn-ocaml-download-template-cmd - :id "demo" + (learn-ocaml-download-template-cmd + :id "demo" :directory learn-ocaml-fixture-directory - :callback (lambda (s) - (should - (= 0 (shell-command - (concat "diff " - learn-ocaml-test-demo-file - " " + :callback (lambda (s) + (should + (= 0 (shell-command + (concat "diff " + learn-ocaml-test-demo-file + " " learn-ocaml-test-template-file)))) - (learn-ocaml-test-remove-demo-file t) - (funcall callback)))))) + (learn-ocaml-test-remove-demo-file t) + (funcall callback)))))) (funcall test done))) - + (ert-deftest-async 6_learn-ocaml-give-exercise-list-test (done) (let ((test (lambda (callback) - (with-temp-buffer - (insert-file-contents learn-ocaml-test-json-file) - (let ((expected (json-read-from-string (buffer-string)))) - (learn-ocaml-give-exercise-list-cmd - (lambda (json) - (should (equal json expected)) - (funcall callback)))))))) + (with-temp-buffer + (insert-file-contents learn-ocaml-test-json-file) + (let ((expected (json-read-from-string (buffer-string)))) + (learn-ocaml-give-exercise-list-cmd + (lambda (json) + (should (equal json expected)) + (funcall callback)))))))) (funcall test done))) @@ -186,11 +192,10 @@ :new-server-value nil :new-token-value token :callback (lambda (_) - (learn-ocaml-give-token-cmd - (lambda (token2) - (should (equal token token2)) - (funcall done)))))))) - + (learn-ocaml-give-token-cmd + (lambda (token2) + (should (equal token token2)) + (funcall done)))))))) (ert-deftest-async 9_learn-ocaml-init-create-token (done) (learn-ocaml-give-token-cmd @@ -200,11 +205,11 @@ :nickname "test" :secret "test" :callback (lambda (_) - (learn-ocaml-give-token-cmd - (lambda (token2) - (should-not (equal previous-token token2)) - (funcall done)))))))) - + (learn-ocaml-give-token-cmd + (lambda (token2) + (should-not (equal previous-token token2)) + (funcall done)))))))) + ;; tests without the config file (ert-deftest-async a10_learn-ocaml-on-load-test-another-token-no-config (done) @@ -215,29 +220,38 @@ :new-server-value learn-ocaml-test-url :new-token-value token :callback (lambda (_) - (learn-ocaml-give-token-cmd - (lambda (token2) - (should (equal token token2)) + (learn-ocaml-give-token-cmd + (lambda (token2) + (should (equal token token2)) ; (learn-ocaml-test-remove-client-file) - (funcall done)))))))) - -(ert-deftest-async a111_learn-ocaml-on-load-test-create-token-no-config (done) + (funcall done)))))))) + +(ert-deftest-async a11_learn-ocaml-on-load-test-create-token-no-config (done) (learn-ocaml-test-remove-client-file) (learn-ocaml-init :new-server-value learn-ocaml-test-url :nickname "test" :secret "test" :callback (lambda (_) - (learn-ocaml-give-token-cmd - (lambda (token2) + (learn-ocaml-give-token-cmd + (lambda (token2) ; (learn-ocaml-test-remove-client-file) - (funcall done)))))) + (funcall done)))))) + +(ert-deftest-async a12_learn-ocaml-test-sign-up (done) + ; FIXME: (learn-ocaml-client-init-server-cmd learn-ocaml-test-url) + (let* ((result (learn-ocaml-client-sign-up-cmd + learn-ocaml-test-url "ErtTest@example.com" "Ocaml123*" "Test" + (escape-secret "")))) + (should (string-equal "A confirmation e-mail has been sent to your address.\nPlease go to your mailbox to finish creating your account,\n then you will be able to sign in.\n" + result))) + (funcall done)) ;; misc tests (setq example-file shell-file-name) ; just to get a filename example -(ert-deftest a12_learn-ocaml-file-path () +(ert-deftest a13_learn-ocaml-file-path () (let* ((path example-file) (dir (file-name-directory path)) (file (file-name-nondirectory path))) diff --git a/tests/use_passwd.json b/tests/use_passwd.json new file mode 100644 index 0000000..a02abbc --- /dev/null +++ b/tests/use_passwd.json @@ -0,0 +1,3 @@ +{ + "use_passwd": true +}