Large-scale Software Development with Elisp

Rocky Bernstein: rocky@gnu.org

Talk Overview

Development Requirements — General

Here are the things that are important to me:

  • Of course, I use GNU Emacs.
  • Of course, I use GNU Emacs.
  • I need to be able to break this program into small chunks or modules.

    Implication: there may be many files.

Development Requirements — Organization

  • I need to be able to run and debug each module in isolation.

    Implication: each module needs to have enough information to pull in whatever other modules it needs

Development Requirements — Developing

  • I want quick development turnaround (edit,test,run...)

  • Implications:
    • This means reducing the amount of "compilation" time
    • it means I don't want to have to "install" code to try modules I am interested in.
    • I may have an "installed" version and a "development" version and ...
    • I want to be able to run from the "development" code with little overhead.

Development Requirements — Testing Continued...

  • I need to be able to test each module in isolation.
  • I need to be able to test in batch
  • Test modules are still modules, so see above.

  • Implications:
    • Because there are many files there are many tests
    • Tests need to be able to be run interactively
    • Tests need to be run in batch

The Internal/External Linking Problem

A (large) Emacs Lisp program uses modules from many places. Some modules reside inside and some outside.

Example: location positioning might be such a thing.

C #includes

Interestingly the very old language, C, got it right. Consider:


#include "stdio.h"
	  

which can also be written as:


#include "./stdio.h"
	  

versus:


#include <stdio.h>
	  

Ruby requires

In Ruby 1.9 and above:


		require "my_module"
	    
versus:


		require_relative "my_module"
	  

load-path is evil

In Emacs Lisp we have load-path which is consulted in load() and require().

Problems with load-path:

  • It is insecure
  • It is complex
  • It is fragile


load-path
("/home/rocky/.emacs.d/elpa/load-relative-20190601.1221"
"/home/rocky/.emacs.d/elpa/lsp-mode-20190601.2003"
"/home/rocky/.emacs.d/elpa/load-relative-20190531.2319"
"/home/rocky/.emacs.d/elpa/lsp-mode-20190531.1911"
"/home/rocky/.emacs.d/elpa/slime-20190531.1534/contrib"
...)
(length load-path) => 185
	  

Introducing load-relative

I wrote load-relative for internal linking. It is available on ELPA and MELPA.

In its simplest form:


(require 'load-relative) ;; pull in library
(require-relative "my-module")
(require-relative "./my-module") ;; same as above
	    

Introducing load-relative-list


(require-relative-list  '("my-module-a" "my-module-b"))
;; The above is the same as:
(require-relative "my-module-a")
(require-relative "my-module-b")
	    

Testing

Schools of Testing:

  • Behavior Driven Development (BDD)
  • Test Driven Development (TDD).

Some Test frameworks.

Emacs Test Unit

I have 66 test files for over 100 Emacs Lisp files. This is more than Emacs 24 using elr.

Let's now dive into a test. This one is from testing test-simple itself:


(load-file "../test-simple.el")
(test-simple-start "test-simple.el")

(note "basic-tests")
(assert-t (memq 'test-simple features) "'test-simple provided")

(assert-nil nil "Knights of ni")
(assert-equal 5 (+ 1 4) "assert-equal")
(assert-raises error (error "you should not see this") "assert-raises")

(end-tests)
	    

Emacs Test Run

Inside GNU Emacs


M-x eval-current-buffer
	    

Inside buffer *test-simple* we have:


test-simple.el
....
 0 failures in 4 assertions (0.00102626 seconds)
	  

Inside a POSIX shell terminal:


$ # after ./configure && make
$ make check-short
make -C test check 2>&1  | ruby make-check-filter.rb
test-simple.el
....
0 failures in 4 assertions (0.000122467 seconds)
..
0 failures in 2 assertions (0.00157458 seconds)
.....
0 failures in 5 assertions (0.000479297 seconds)
	    

Links