aboutsummaryrefslogtreecommitdiff
path: root/gnu/tests/emacs.scm
blob: f15eec70dbe70b27e4d6bf1620e380b2a0081da9 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2024 Liliana Marie Prikler <liliana.prikler@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix 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 Guix 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 Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu tests emacs)
  #:use-module (gnu tests)
  #:use-module (gnu packages emacs)
  #:use-module (gnu packages vim)
  #:use-module (gnu services)
  #:use-module (gnu system)
  #:use-module (gnu system vm)
  #:use-module (guix packages)
  #:use-module (guix gexp)
  #:use-module (srfi srfi-1)
  #:export (%test-emacs-native-comp-replacable))

(define (run-native-comp-replacable-test old-emacs new-emacs)
  (define vm (virtual-machine (marionette-operating-system %simple-os)))

  (define test
    (with-imported-modules '((gnu build marionette))
      #~(begin
          (use-modules (gnu build marionette)
                       (srfi srfi-1)
                       (srfi srfi-64))

          (define marionette (make-marionette (list #$vm)))
          (define (marionette-emacs-eval emacs code)
            (marionette-eval
             `(begin
                (use-modules (ice-9 rdelim) (ice-9 popen))
                (read-line
                 (open-pipe*
                  OPEN_READ
                  ,emacs "--batch"
                  ,(string-append "--eval=" code))))
             marionette))

          (define (emacs-native-comp-dir emacs)
            (marionette-emacs-eval emacs "(princ comp-native-version-dir)"))
          (define (emacs-abi-hash emacs)
            (marionette-emacs-eval emacs "(princ comp-abi-hash)"))
          (define (emacs-effective-version emacs)
            (marionette-emacs-eval
             emacs
             "(princ
               (format \"%s.%s\" emacs-major-version emacs-minor-version))"))

          (define old-emacs-bin #$(file-append old-emacs "/bin/emacs"))
          (define new-emacs-bin #$(file-append new-emacs "/bin/emacs"))

          (test-runner-current (system-test-runner #$output))
          (test-begin "emacs-native-comp-replacable")
          (test-equal "comp-abi-hash"
            (emacs-abi-hash old-emacs-bin)
            (emacs-abi-hash new-emacs-bin))
          (test-equal "native-comp-dir"
            (emacs-native-comp-dir old-emacs-bin)
            (emacs-native-comp-dir new-emacs-bin))
          (test-assert "old emacs has hierarchical layout"
            (file-exists?
             (string-append #$old-emacs "/lib/emacs/"
                            (emacs-effective-version old-emacs-bin)
                            "/native-lisp/"
                            (emacs-native-comp-dir old-emacs-bin)
                            "/preloaded/emacs-lisp/comp.eln")))
          (test-assert "new emacs has hierarchical layout"
            (file-exists?
             (string-append #$new-emacs "/lib/emacs/"
                            (emacs-effective-version new-emacs-bin)
                            "/native-lisp/"
                            (emacs-native-comp-dir new-emacs-bin)
                            "/preloaded/emacs-lisp/comp.eln")))
          (test-end))))

  (gexp->derivation "emacs-native-comp-compatible" test))

(define (package-without-replacement pkg)
  (package (inherit pkg) (replacement #f)))

(define %test-emacs-native-comp-replacable
  (system-test
   (name "emacs-native-comp")
   (description "Test whether an emacs replacement (if any) is valid.")
   (value (run-native-comp-replacable-test
           (package-without-replacement emacs)
           emacs))))
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
#!@GUILE@ \
--no-auto-compile -s
!#

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020, 2021 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev>
;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix 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 Guix 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 Guix.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This script stages and commits changes to package definitions.

;;; Code:

(import (sxml xpath)
        (srfi srfi-1)
        (srfi srfi-2)
        (srfi srfi-9)
        (srfi srfi-11)
        (srfi srfi-26)
        (ice-9 format)
        (ice-9 popen)
        (ice-9 match)
        (ice-9 rdelim)
        (ice-9 regex)
        (ice-9 textual-ports)
        (guix gexp))

(define* (break-string str #:optional (max-line-length 70))
  "Break the string STR into lines that are no longer than MAX-LINE-LENGTH.
Return a single string."
  (define (restore-line words)
    (string-join (reverse words) " "))
  (if (<= (string-length str) max-line-length)
      str
      (let ((words+lengths (map (lambda (word)
                                  (cons word (string-length word)))
                                (string-tokenize str))))
        (match (fold (match-lambda*
                       (((word . length)
                         (count current lines))
                        (let ((new-count (+ count length 1)))
                          (if (< new-count max-line-length)
                              (list new-count
                                    (cons word current)
                                    lines)
                              (list length
                                    (list word)
                                    (cons (restore-line current) lines))))))
                     '(0 () ())
                     words+lengths)
          ((_ last-words lines)
           (string-join (reverse (cons (restore-line last-words) lines))
                        "\n"))))))

(define* (break-string-with-newlines str #:optional (max-line-length 70))
  "Break the lines of string STR into lines that are no longer than
MAX-LINE-LENGTH. Return a single string."
  (string-join (map (cut break-string <> max-line-length)
                    (string-split str #\newline))
               "\n"))

(define (read-excursion port)
  "Read an expression from PORT and reset the port position before returning
the expression."
  (let ((start (ftell port))
        (result (read port)))
    (seek port start SEEK_SET)
    result))

(define (surrounding-sexp port line-no)
  "Return the top-level S-expression surrounding the change at line number
LINE-NO in PORT."
  (let loop ((i (1- line-no))
             (last-top-level-sexp #f))
    (if (zero? i)
        last-top-level-sexp
        (match (peek-char port)
          (#\(
           (let ((sexp (read-excursion port)))
             (read-line port)
             (loop (1- i) sexp)))
          (_
           (read-line port)
           (loop (1- i) last-top-level-sexp))))))

(define-record-type <hunk>
  (make-hunk file-name
             old-line-number
             new-line-number
             diff-lines
             definition?)
  hunk?
  (file-name       hunk-file-name)
  ;; Line number before the change
  (old-line-number hunk-old-line-number)
  ;; Line number after the change
  (new-line-number hunk-new-line-number)
  ;; The full diff to be used with "git apply --cached"
  (diff-lines hunk-diff-lines)
  ;; Does this hunk add a definition?
  (definition? hunk-definition?))

(define* (hunk->patch hunk #:optional (port (current-output-port)))
  (let ((file-name (hunk-file-name hunk)))
    (format port
            "diff --git a/~a b/~a~%--- a/~a~%+++ b/~a~%~a"
            file-name file-name file-name file-name
            (string-join (hunk-diff-lines hunk) ""))))

(define (diff-info)
  "Read the diff and return a list of <hunk> values."
  (let ((port (open-pipe* OPEN_READ
                          "git" "diff-files"
                          "--no-prefix"
                          ;; Only include one context line to avoid lumping in
                          ;; new definitions with changes to existing
                          ;; definitions.
                          "--unified=1"
                          "gnu")))
    (define (extract-line-number line-tag)
      (abs (string->number
            (car (string-split line-tag #\,)))))
    (define (read-hunk)
      (let loop ((lines '())
                 (definition? #false))
        (let ((line (read-line port 'concat)))
          (cond
           ((eof-object? line)
            (values (reverse lines) definition?))
           ((or (string-prefix? "@@ " line)
                (string-prefix? "diff --git" line))
            (unget-string port line)
            (values (reverse lines) definition?))
           (else
            (loop (cons line lines)
                  (or definition?
                      (string-prefix? "+(define" line))))))))
    (define info
      (let loop ((acc '())
                 (file-name #f))
        (let ((line (read-line port)))
          (cond
           ((eof-object? line) acc)
           ((string-prefix? "--- " line)
            (match (string-split line #\space)
              ((_ file-name)
               (loop acc file-name))))
           ((string-prefix? "@@ " line)
            (match (string-split line #\space)
              ((_ old-start new-start . _)
               (let-values
                   (((diff-lines definition?) (read-hunk)))
                 (loop (cons (make-hunk file-name
                                        (extract-line-number old-start)
                                        (extract-line-number new-start)
                                        (cons (string-append line "\n")
                                              diff-lines)
                                        definition?) acc)
                       file-name)))))
           (else (loop acc file-name))))))
    (close-pipe port)
    info))

(define (lines-to-first-change hunk)
  "Return the number of diff lines until the first change."
  (1- (count (lambda (line)
               ((negate char-set-contains?)
                (char-set #\+ #\-)
                (string-ref line 0)))
             (hunk-diff-lines hunk))))

(define (old-sexp hunk)
  "Using the diff information in HUNK return the unmodified S-expression
corresponding to the top-level definition containing the staged changes."
  ;; TODO: We can't seek with a pipe port...
  (let* ((port (open-pipe* OPEN_READ
                           "git" "cat-file" "-p" (string-append
                                                  "HEAD:"
                                                  (hunk-file-name hunk))))
         (contents (get-string-all port)))
    (close-pipe port)
    (call-with-input-string contents
      (lambda (port)
        (surrounding-sexp port
                          (+ (lines-to-first-change hunk)
                             (hunk-old-line-number hunk)))))))

(define (new-sexp hunk)
  "Using the diff information in HUNK return the modified S-expression
corresponding to the top-level definition containing the staged changes."
  (call-with-input-file (hunk-file-name hunk)
    (lambda (port)
      (surrounding-sexp port
                        (+ (lines-to-first-change hunk)
                           (hunk-new-line-number hunk))))))

(define* (change-commit-message file-name old new #:optional (port (current-output-port)))
  "Print ChangeLog commit message for changes between OLD and NEW."
  (define (get-values expr field)
    (match ((sxpath `(// ,field quasiquote *)) expr)
      (()
       ;; New-style plain lists
       (match ((sxpath `(// ,field list *)) expr)
         ((inner) inner)
         (_ '())))
      ;; Old-style labelled inputs
      ((first . rest)
       (map cadadr first))))
  (define (listify items)
    (match items
      ((one) one)
      ((one two)
       (string-append one " and " two))
      ((one two . more)
       (string-append (string-join (drop-right items 1) ", ")
                      ", and " (first (take-right items 1))))))
  (define variable-name
    (second old))
  (define version
    (and=> ((sxpath '(// version *any*)) new)
           first))
  (format port
          "gnu: ~a: Update to ~a.~%~%* ~a (~a): Update to ~a.~%"
          variable-name version file-name variable-name version)
  (for-each (lambda (field)
              (let ((old-values (get-values old field))
                    (new-values (get-values new field)))
                (or (equal? old-values new-values)
                    (let ((removed (lset-difference equal? old-values new-values))
                          (added (lset-difference equal? new-values old-values)))
                      (format port
                              "[~a]: ~a~%" field
                              (break-string
                               (match (list (map symbol->string removed)
                                            (map symbol->string added))
                                 ((() added)
                                  (format #f "Add ~a."
                                          (listify added)))
                                 ((removed ())
                                  (format #f "Remove ~a."
                                          (listify removed)))
                                 ((removed added)
                                  (format #f "Remove ~a; add ~a."
                                          (listify removed)
                                          (listify added))))))))))
            '(inputs propagated-inputs native-inputs)))

(define* (add-commit-message file-name variable-name #:optional (port (current-output-port)))
  "Print ChangeLog commit message for a change to FILE-NAME adding a definition."
  (format port
          "gnu: Add ~a.~%~%* ~a (~a): New variable.~%"
          variable-name file-name variable-name))

(define* (custom-commit-message file-name variable-name message changelog
                                #:optional (port (current-output-port)))
  "Print custom commit message for a change to VARIABLE-NAME in FILE-NAME, using
MESSAGE as the commit message and CHANGELOG as the body of the ChangeLog
entry. If CHANGELOG is #f, the commit message is reused. If CHANGELOG already
contains ': ', no colon is inserted between the location and body of the
ChangeLog entry."
  (define (trim msg)
    (string-trim-right (string-trim-both msg) (char-set #\.)))

  (define (changelog-has-location? changelog)
    (->bool (string-match "^[[:graph:]]+:[[:blank:]]" changelog)))

  (let* ((message (trim message))
         (changelog (if changelog (trim changelog) message))
         (message/f (format #f "gnu: ~a: ~a." variable-name message))
         (changelog/f (if (changelog-has-location? changelog)
                          (format #f "* ~a (~a)~a."
                                  file-name variable-name changelog)
                          (format #f "* ~a (~a): ~a."
                                  file-name variable-name changelog))))
    (format port
            "~a~%~%~a~%"
            (break-string-with-newlines message/f 72)
            (break-string-with-newlines changelog/f 72))))

(define (add-copyright-line line)
  "Add the copyright line on LINE to the previous commit."
  (let ((author (match:substring
                 (string-match "^\\+;;; Copyright ©[^[:alpha:]]+(.*)$" line)
                 1)))
    (format
     (current-output-port) "Amend and add copyright line for ~a~%" author)
    (system* "git" "commit" "--amend" "--no-edit")))

(define (group-hunks-by-sexp hunks)
  "Return a list of pairs associating all hunks with the S-expression they are
modifying."
  (fold (lambda (sexp hunk acc)
          (match acc
            (((previous-sexp . hunks) . rest)
             (if (equal? sexp previous-sexp)
                 (cons (cons previous-sexp
                             (cons hunk hunks))
                       rest)
                 (cons (cons sexp (list hunk))
                       acc)))
            (_
             (cons (cons sexp (list hunk))
                   acc))))
        '()
        (map new-sexp hunks)
        hunks))

(define (new+old+hunks hunks)
  (map (match-lambda
         ((new . hunks)
          (cons* new (old-sexp (first hunks)) hunks)))
       (group-hunks-by-sexp hunks)))

(define %delay 1000)

(define (main . args)
  (define* (change-commit-message* file-name old new #:rest rest)
    (let ((changelog #f))
      (match args
        ((or (message changelog) (message))
         (apply custom-commit-message
                file-name (second old) message changelog rest))
        (_
         (apply change-commit-message file-name old new rest)))))

  (match (diff-info)
    (()
     (display "Nothing to be done.\n" (current-error-port)))
    (hunks
     (let-values
         (((definitions changes)
           (partition hunk-definition? hunks)))

       ;; Additions.
       (for-each (lambda (hunk)
                   (and-let*
                       ((define-line (find (cut string-prefix? "+(define" <>)
                                           (hunk-diff-lines hunk)))
                        (variable-name (and=> (string-tokenize define-line) second)))
                     (add-commit-message (hunk-file-name hunk) variable-name)
                     (let ((port (open-pipe* OPEN_WRITE                   
                                             "git" "apply"                
                                             "--cached"                   
                                             "--unidiff-zero")))          
                       (hunk->patch hunk port)                            
                       (unless (eqv? 0 (status:exit-val (close-pipe port))) 
                         (error "Cannot apply")))

                     (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
                       (add-commit-message (hunk-file-name hunk)
                                           variable-name port)
                       (usleep %delay)
                       (unless (eqv? 0 (status:exit-val (close-pipe port)))
                         (error "Cannot commit"))))
                   (usleep %delay))
                 definitions)

       ;; Changes.
       (for-each (match-lambda
                   ((new old . hunks)
                    (for-each (lambda (hunk)
                                (let ((port (open-pipe* OPEN_WRITE
                                                        "git" "apply"
                                                        "--cached"
                                                        "--unidiff-zero")))
                                  (hunk->patch hunk port)
                                  (unless (eqv? 0 (status:exit-val (close-pipe port)))
                                    (error "Cannot apply")))
                                (usleep %delay))
                              hunks)
                    (define copyright-line
                      (any (lambda (line) (and=> (string-prefix? "+;;; Copyright ©" line)
                                              (const line)))
                                (hunk-diff-lines (first hunks))))
                    (cond
                     (copyright-line
                      (add-copyright-line copyright-line))
                     (else
                      (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
                        (change-commit-message* (hunk-file-name (first hunks))
                                                old new)
                      (change-commit-message* (hunk-file-name (first hunks))
                                              old new
                                              port)
                      (usleep %delay)
                      (unless (eqv? 0 (status:exit-val (close-pipe port)))
                        (error "Cannot commit")))))))
                 ;; XXX: we recompute the hunks here because previous
                 ;; insertions lead to offsets.
                 (new+old+hunks (diff-info)))))))

(apply main (cdr (command-line)))