;;; thread-tests.el --- tests for threads. -*- lexical-binding: t -*- ;; Copyright (C) 2012-2024 Free Software Foundation, Inc. ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Code: (require 'thread) ;; Declare the functions in case Emacs has been configured --without-threads. (declare-function all-threads "thread.c" ()) (declare-function condition-mutex "thread.c" (cond)) (declare-function condition-name "thread.c" (cond)) (declare-function condition-notify "thread.c" (cond &optional all)) (declare-function condition-wait "thread.c" (cond)) (declare-function current-thread "thread.c" ()) (declare-function make-condition-variable "thread.c" (mutex &optional name)) (declare-function make-mutex "thread.c" (&optional name)) (declare-function make-thread "thread.c" (function &optional name)) (declare-function mutex-lock "thread.c" (mutex)) (declare-function mutex-unlock "thread.c" (mutex)) (declare-function thread--blocker "thread.c" (thread)) (declare-function thread-live-p "thread.c" (thread)) (declare-function thread-join "thread.c" (thread)) (declare-function thread-last-error "thread.c" (&optional cleanup)) (declare-function thread-name "thread.c" (thread)) (declare-function thread-signal "thread.c" (thread error-symbol data)) (declare-function thread-yield "thread.c" ()) (defvar main-thread) (ert-deftest threads-is-one () "Test for existence of a thread." (skip-unless (featurep 'threads)) (should (current-thread))) (ert-deftest threads-threadp () "Test of threadp." (skip-unless (featurep 'threads)) (should (threadp (current-thread)))) (ert-deftest threads-type () "Test of thread type." (skip-unless (featurep 'threads)) (should (eq (type-of (current-thread)) 'thread))) (ert-deftest threads-name () "Test for name of a thread." (skip-unless (featurep 'threads)) (should (string= "hi bob" (thread-name (make-thread #'ignore "hi bob"))))) (ert-deftest threads-live () "Test for thread liveness." (skip-unless (featurep 'threads)) (should (thread-live-p (make-thread #'ignore)))) (ert-deftest threads-all-threads () "Simple test for `all-threads'." (skip-unless (featurep 'threads)) (should (listp (all-threads)))) (ert-deftest threads-main-thread () "Simple test for `all-threads'." (skip-unless (featurep 'threads)) (should (eq main-thread (car (all-threads))))) (defvar threads-test-global nil) (defun threads-test-thread1 () (setq threads-test-global 23)) (ert-deftest threads-basic () "Basic thread test." (skip-unless (featurep 'threads)) (should (progn (setq threads-test-global nil) (make-thread #'threads-test-thread1) (while (not threads-test-global) (thread-yield)) threads-test-global))) (ert-deftest threads-join () "Test of `thread-join'." (skip-unless (featurep 'threads)) (should (progn (setq threads-test-global nil) (let ((thread (make-thread #'threads-test-thread1))) (and (= (thread-join thread) 23) (= threads-test-global 23) (not (thread-live-p thread))))))) (ert-deftest threads-join-self () "Cannot `thread-join' the current thread." (skip-unless (featurep 'threads)) (should-error (thread-join (current-thread)))) (ert-deftest threads-join-error () "Test of error signaling from `thread-join'." :tags '(:unstable) (skip-unless (featurep 'threads)) (let ((thread (make-thread #'threads-call-error))) (while (thread-live-p thread) (thread-yield)) (should-error (thread-join thread)))) (defvar threads-test-binding nil) (defun threads-test-thread2 () (let ((threads-test-binding 23)) (thread-yield)) (setq threads-test-global 23)) (ert-deftest threads-let-binding () "Simple test of threads and let bindings." (skip-unless (featurep 'threads)) (should (progn (setq threads-test-global nil) (make-thread #'threads-test-thread2) (while (not threads-test-global) (thread-yield)) (and (not threads-test-binding) threads-test-global)))) (ert-deftest threads-mutexp () "Simple test of `mutexp'." (skip-unless (featurep 'threads)) (should-not (mutexp 'hi))) (ert-deftest threads-mutexp-2 () "Another simple test of `mutexp'." (skip-unless (featurep 'threads)) (should (mutexp (make-mutex)))) (ert-deftest threads-mutex-type () "type-of mutex." (skip-unless (featurep 'threads)) (should (eq (type-of (make-mutex)) 'mutex))) (ert-deftest threads-mutex-lock-unlock () "Test `mutex-lock' and unlock." (skip-unless (featurep 'threads)) (should (let ((mx (make-mutex))) (mutex-lock mx) (mutex-unlock mx) t))) (ert-deftest threads-mutex-recursive () "Test mutex recursion." (skip-unless (featurep 'threads)) (should (let ((mx (make-mutex))) (mutex-lock mx) (mutex-lock mx) (mutex-unlock mx) (mutex-unlock mx) t))) (defvar threads-mutex nil) (defvar threads-mutex-key nil) (defun threads-test-mlock () (mutex-lock threads-mutex) (setq threads-mutex-key 23) (while threads-mutex-key (thread-yield)) (mutex-unlock threads-mutex)) (ert-deftest threads-mutex-contention () "Test of mutex contention." (skip-unless (featurep 'threads)) (should (progn (setq threads-mutex (make-mutex)) (setq threads-mutex-key nil) (make-thread #'threads-test-mlock) ;; Wait for other thread to get the lock. (while (not threads-mutex-key) (thread-yield)) ;; Try now. (setq threads-mutex-key nil) (mutex-lock threads-mutex) (mutex-unlock threads-mutex) t))) (defun threads-test-mlock2 () (setq threads-mutex-key 23) (mutex-lock threads-mutex)) (ert-deftest threads-mutex-signal () "Test signaling a blocked thread." (skip-unless (featurep 'threads)) (should-error (progn (setq threads-mutex (make-mutex)) (setq threads-mutex-key nil) (mutex-lock threads-mutex) (let ((thr (make-thread #'threads-test-mlock2))) (while (not threads-mutex-key) (thread-yield)) (thread-signal thr 'quit nil) ;; `quit' is not caught by `should-error'. We must indicate it. (condition-case nil (thread-join thr) (quit (signal 'error nil))))))) (defun threads-test-io-switch () (setq threads-test-global 23)) (ert-deftest threads-io-switch () "Test that `accept-process-output' causes thread switch." (skip-unless (featurep 'threads)) (should (progn (setq threads-test-global nil) (make-thread #'threads-test-io-switch) (while (not threads-test-global) (accept-process-output nil 1)) threads-test-global))) (ert-deftest threads-condvarp () "Simple test of `condition-variable-p'." (skip-unless (featurep 'threads)) (should-not (condition-variable-p 'hi))) (ert-deftest threads-condvarp-2 () "Another simple test of `condition-variable-p'." (skip-unless (featurep 'threads)) (should (condition-variable-p (make-condition-variable (make-mutex))))) (ert-deftest threads-condvar-type () "type-of condvar" (skip-unless (featurep 'threads)) (should (eq (type-of (make-condition-variable (make-mutex))) 'condition-variable))) (ert-deftest threads-condvar-mutex () "Simple test of `condition-mutex'." (skip-unless (featurep 'threads)) (should (let ((m (make-mutex))) (eq m (condition-mutex (make-condition-variable m)))))) (ert-deftest threads-condvar-name () "Simple test of `condition-name'." (skip-unless (featurep 'threads)) (should (eq nil (condition-name (make-condition-variable (make-mutex)))))) (ert-deftest threads-condvar-name-2 () "Another simple test of `condition-name'." (skip-unless (featurep 'threads)) (should (string= "hi bob" (condition-name (make-condition-variable (make-mutex) "hi bob"))))) (defun threads-call-error () "Call `error'." (error "Error is called")) ;; This signals an error internally; the error should be caught. (defun threads-custom () (defcustom threads-custom-face 'highlight "Face used for thread customizations." :type 'face :group 'widget-faces)) (ert-deftest threads-errors () "Test what happens when a thread signals an error." (skip-unless (featurep 'threads)) (let (th1 th2) (setq th1 (make-thread #'threads-call-error "call-error")) (should (threadp th1)) (while (thread-live-p th1) (thread-yield)) (should (equal (thread-last-error) '(error "Error is called"))) (should (equal (thread-last-error 'cleanup) '(error "Error is called"))) (should-not (thread-last-error)) (setq th2 (make-thread #'threads-custom "threads-custom")) (should (threadp th2)))) (ert-deftest threads-sticky-point () "Test bug #25165 with point movement in cloned buffer." (skip-unless (featurep 'threads)) (with-temp-buffer (insert "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") (goto-char (point-min)) (clone-indirect-buffer nil nil) (forward-char 20) (sit-for 1) (should (= (point) 21)))) (ert-deftest threads-signal-early () "Test signaling a thread as soon as it is started by the OS." (skip-unless (featurep 'threads)) (let ((thread (make-thread (lambda () (while t (thread-yield)))))) (thread-signal thread 'error nil) (sit-for 1) (should-not (thread-live-p thread)) (should (equal (thread-last-error) '(error))))) (ert-deftest threads-signal-main-thread () "Test signaling the main thread." (skip-unless (featurep 'threads)) ;; We cannot use `ert-with-message-capture', because threads do not ;; know let-bound variables. (with-current-buffer "*Messages*" (let (buffer-read-only) (erase-buffer)) (let ((thread (make-thread (lambda () (thread-signal main-thread 'error nil))))) (while (thread-live-p thread) (thread-yield)) (read-event nil nil 0.1) ;; No error has been raised, which is part of the test. (should (string-match (format-message "Error %s: (error nil)" thread) (buffer-string )))))) (defvar threads-condvar nil) (defun threads-test-condvar-wait () ;; Wait for condvar to be notified. (with-mutex (condition-mutex threads-condvar) (condition-wait threads-condvar)) ;; Wait again, it will be signaled. (with-mutex (condition-mutex threads-condvar) (condition-wait threads-condvar))) (ert-deftest threads-condvar-wait () "Test waiting on conditional variable." (skip-unless (featurep 'threads)) (let ((cv-mutex (make-mutex)) new-thread) ;; We could have spurious threads from the previous tests still ;; running; wait for them to die. (while (> (length (all-threads)) 1) (thread-yield)) (setq threads-condvar (make-condition-variable cv-mutex)) (setq new-thread (make-thread #'threads-test-condvar-wait)) ;; Make sure new-thread is alive. (should (thread-live-p new-thread)) (should (= (length (all-threads)) 2)) ;; Wait for new-thread to become blocked on the condvar. (while (not (eq (thread--blocker new-thread) threads-condvar)) (thread-yield)) ;; Notify the waiting thread. (with-mutex cv-mutex (condition-notify threads-condvar t)) ;; Allow new-thread to process the notification. (sleep-for 0.1) ;; Make sure the thread is still there. This used to fail due to ;; a bug in thread.c:condition_wait_callback. (should (thread-live-p new-thread)) (should (= (length (all-threads)) 2)) (should (eq (thread--blocker new-thread) threads-condvar)) ;; Signal the thread. (thread-signal new-thread 'error '("Die, die, die!")) (sleep-for 0.1) ;; Make sure the thread died. (should (= (length (all-threads)) 1)) (should (equal (thread-last-error) '(error "Die, die, die!"))))) (ert-deftest threads-test-bug33073 () (skip-unless (fboundp 'make-thread)) (let ((th (make-thread 'ignore))) (should-not (equal th main-thread)))) (defvar threads-test--var 'global) (ert-deftest threads-test-bug48990 () (skip-unless (fboundp 'make-thread)) (let ((buf1 (generate-new-buffer " thread-test")) (buf2 (generate-new-buffer " thread-test"))) (with-current-buffer buf1 (setq-local threads-test--var 'local1)) (with-current-buffer buf2 (setq-local threads-test--var 'local2)) (let ((seen nil)) (with-current-buffer buf1 (should (eq threads-test--var 'local1)) (make-thread (lambda () (setq seen threads-test--var)))) (with-current-buffer buf2 (should (eq threads-test--var 'local2)) (let ((threads-test--var 'let2)) (should (eq threads-test--var 'let2)) (while (not seen) (thread-yield)) (should (eq threads-test--var 'let2)) (should (eq seen 'local1))) (should (eq threads-test--var 'local2))) (should (eq threads-test--var 'global))))) ;;; thread-tests.el ends here