]> git.ozlabs.org Git - ccan/commitdiff
ccanlint: enhance and streamline "output" testing lines.
authorRusty Russell <rusty@rustcorp.com.au>
Mon, 28 Sep 2015 03:14:40 +0000 (12:44 +0930)
committerRusty Russell <rusty@rustcorp.com.au>
Tue, 29 Sep 2015 00:39:09 +0000 (10:09 +0930)
1) Require "" around input
2) Make them optional around output: if not there, loose match whitespace
3) Handle \n in output.
4) Document that "Given xxx" is optional.
5) Reject any non-matching comment lines starting with "given" or "outputs"
6) Fix missed test in ccan/cast

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
19 files changed:
ccan/array_size/_info
ccan/autodata/_info
ccan/cast/_info
ccan/crc/_info
ccan/io/_info
ccan/jmap/_info
ccan/objset/_info
ccan/rfc822/_info
ccan/siphash/_info
ccan/siphash/siphash.h
ccan/strmap/_info
ccan/strset/_info
ccan/take/_info
ccan/tal/link/_info
ccan/tal/str/str.h
ccan/tcon/_info
doc/ccanlint.1
doc/ccanlint.1.txt
tools/ccanlint/tests/examples_run.c

index b8a9a85094b19a7e3e2e991a8c7c23b4cc2412ac..69570f34c8dd73cf86473ce6430a91a840c9c6fc 100644 (file)
@@ -12,7 +12,7 @@
  * macro or constant.
  *
  * Example:
- *     // Outputs "Initialized 32 values"
+ *     // Outputs "Initialized 32 values\n"
  *     #include <ccan/array_size/array_size.h>
  *     #include <stdlib.h>
  *     #include <stdio.h>
index a101276d7720ae1988664f20acc3fd31a26769d0..507de1a1ed9e2452a023872931f2a20a256d1def 100644 (file)
@@ -92,8 +92,8 @@
  *             printf("verbose mode on\n");
  *     return 0;
  * }
- * // Given -v outputs 'verbose mode on'
- * // Given -v -C / outputs 'chdir to /. verbose mode on'
+ * // Given "-v" outputs "verbose mode on\n"
+ * // Given "-v -C /" outputs "chdir to /. verbose mode on\n"
  */
 int main(int argc, char *argv[])
 {
index f5cd37fc337bf42159f3f4fa66fc846031eb6d20..b09e0344e49d6ee570e9b70dbbe98e75393d8847 100644 (file)
@@ -20,7 +20,7 @@
  * License: LGPL (v2.1 or any later version)
  *
  * Example:
- *     // Given "test" contains "3 t's in 'test string'
+ *     // Given "test" output contains "3 t's in 'test string'"
  *     #include <ccan/cast/cast.h>
  *     #include <stdint.h>
  *     #include <stdio.h>
index c59d58ec8e19eb847a7d54d8926eefa0890e3a2c..8a47db969fbe9d5804273696cca3db64ef9fa363 100644 (file)
@@ -16,7 +16,7 @@
  *     #include <stdio.h>
  *     #include <stdlib.h>
  *
- *     // Given IHATEMATH outputs 0x98a3b8df
+ *     // Given "IHATEMATH" outputs 0x98a3b8df
  *     int main(int argc, char *argv[])
  *     {
  *             if (argc != 2) {
index 42b71d9e6e9f18a48b278ddb5beed8a7ec8c4396..fe49df5c8d0418094b3630af88bc49e5f9493a5b 100644 (file)
@@ -11,7 +11,7 @@
  * plans.
  *
  * Example:
- * // Given tr A-Z a-z outputs tr a-z a-z
+ * // Given "tr A-Z a-z" outputs tr a-z a-z
  * #include <ccan/io/io.h>
  * #include <ccan/err/err.h>
  * #include <assert.h>
index 739e60af7a105bf7f08c84e0242f19b448880289..ed1947fd32cc6bd40bae836240e1c0ce93d1060c 100644 (file)
@@ -80,9 +80,9 @@
  *     jmap_free(arg);
  *     return 0;
  * }
- * // Given "--help" output contains "Arg 1 ('--help') is a long opt of 4 chars"
- * // Given "-h" output contains "Arg 1 ('-h') is a short opt of 1 chars"
- * // Given "foo" output contains "Arg 1 ('foo') is a normal arg of 3 chars"
+ * // Given "--help" output contains "Arg 1 ('--help') is a long opt of 4 chars\n"
+ * // Given "-h" output contains "Arg 1 ('-h') is a short opt of 1 chars\n"
+ * // Given "foo" output contains "Arg 1 ('foo') is a normal arg of 3 chars\n"
  *
  * License: LGPL (v2.1 or any later version)
  * Author: Rusty Russell <rusty@rustcorp.com.au>
index 5831f584da9e3707710c6141f34cbaeac6da3e98..967764e7e86cb9de97c622e9b297e74071b85907 100644 (file)
@@ -41,9 +41,9 @@
  *             }
  *             return 0;
  *     }
- *     // Given 'a b c' outputs No arguments start with -.
- *     // Given 'a -b c' outputs 2,
- *     // Given 'a -b -c d' outputs 2,3,
+ *     // Given "a b c" outputs No arguments start with -.
+ *     // Given "a -b c" outputs 2,
+ *     // Given "a -b -c d" outputs 2,3,
  */
 int main(int argc, char *argv[])
 {
index 680f4991981fb3b5f8663b6981addbc6052dde38..5a0a9c6a6080c050c35095867019026d0ce02ce5 100644 (file)
@@ -25,9 +25,9 @@
  * it too).
  *
  * Example:
- *     // Given '' outputs 'body'
- *     // Given 'From' outputs ' <from@example.com>'
- *     // Given 'To' outputs ' <to@example.com>'
+ *     // Outputs "body\n"
+ *     // Given "From" outputs <from@example.com>
+ *     // Given "To" outputs <to@example.com>
  *     char buf[] = "From: <from@example.com>\n"
  *                  "To: <to@example.com>\n\n"
  *                   "body\n";
@@ -35,7 +35,7 @@
  *     struct bytestring out;
  *
  *     msg = rfc822_start(NULL, buf, sizeof(buf));
- *     if (!argv[1] || !argv[1][0])
+ *     if (!argv[1])
  *             out = rfc822_body(msg);
  *     else {
  *             struct rfc822_header *hdr;
index d42ddab4c698800f5c390d43ff660f31ca866cfd..7aa317166d22dd3ec51bafc8dc8a7dd085a417f8 100644 (file)
@@ -23,7 +23,7 @@
  * Returns one 64-bit word as the hash function result.
  *
  * Example:
- *     // Outputs "cf2794e0277187b7"
+ *     // Outputs cf2794e0277187b7
  *     #include <stdio.h>
  *     #include <ccan/siphash/siphash.h>
  *
index 55a391d253549a415c1bb9a31a2f7f2856f90c96..6f39f9d586f9c6c8957cc30b564eb17bfdf6c787 100644 (file)
@@ -28,7 +28,7 @@
  * Returns one 64-bit word as the hash function result.
  *
  * Example:
- *     // Outputs "cf2794e0277187b7"
+ *     // Outputs cf2794e0277187b7
  *     #include <stdio.h>
  *     #include <ccan/siphash/siphash.h>
  *
index 33176b304e8522b9fb5534f773db322b904addf7..55319a82db53009bd3ab0b6cdd5830bd73cfd4d7 100644 (file)
@@ -40,9 +40,9 @@
  *     printf("\n");
  *     return 0;
  * }
- * // Given 'foo' outputs 'foo at 1. '
- * // Given 'foo bar' outputs 'bar at 2. foo at 1. '
- * // Given 'foo foo bar zebra' outputs 'bar at 3. foo at 1. zebra at 4. '
+ * // Given "foo" outputs "foo at 1. \n"
+ * // Given "foo bar" outputs "bar at 2. foo at 1. \n"
+ * // Given "foo foo bar zebra" outputs "bar at 3. foo at 1. zebra at 4. \n"
  */
 int main(int argc, char *argv[])
 {
index 3f4f7733f23e6b926891835ff859e37133d5e5cf..982a9c0f851ae7d5485b42cc2e53578125613e14 100644 (file)
@@ -45,8 +45,8 @@
  *             printf("\n");
  *             return 0;
  *     }
- *     // Given "foo bar" outputs "bar foo "
- *     // Given "foo foo bar" outputs "bar foo "
+ *     // Given "foo bar" outputs "bar foo \n"
+ *     // Given "foo foo bar" outputs "bar foo \n"
  *
  * License: CC0 (but some dependencies are LGPL!)
  * Author: Rusty Russell <rusty@rustcorp.com.au>
index a3ac53921aeeec693996c486f65620d72cf95018..693824664802aebdeef970c4de29f23fb4fc0efb 100644 (file)
@@ -13,7 +13,7 @@
  * License: CC0 (Public domain)
  *
  * Example:
- *     // Given foo/bar.c outputs basename is bar.c
+ *     // Given "foo/bar.c" outputs basename is bar.c
  *     #include <ccan/take/take.h>
  *     #include <string.h>
  *
index 6962cb45fc033bfbca291a95782b90bab4e87626..db2ad61454687f4f9ba3a4b885df4ff2e77f75cc 100644 (file)
@@ -14,8 +14,8 @@
  *     // Silly program which keeps a cache of uppercased strings.
  *     // The cache wants to keep strings around even after they may have
  *     // been "freed" by the caller.
- *     // Given 'hello' outputs '1 cache hits HELLO '
- *     // Given 'hello hello there' outputs '4 cache hits HELLO HELLO THERE '
+ *     // Given "hello" outputs "1 cache hits HELLO \n"
+ *     // Given "hello hello there" outputs "4 cache hits HELLO HELLO THERE \n"
  *     #include <stdio.h>
  *     #include <err.h>
  *     #include <string.h>
index 0fe542cf235eeb9c08119f0de14ced6f6050a0ea..0c1821331e6a733b7a43fe3491e19b9065947eb8 100644 (file)
@@ -164,9 +164,9 @@ char *tal_strjoin(const void *ctx, char *strings[], const char *delim,
  *     regcomp(3), regex(3).
  *
  * Example:
- *     // Given 'My name is Rusty' outputs 'Hello Rusty!'
- *     // Given 'my first name is Rusty Russell' outputs 'Hello Rusty Russell!'
- *     // Given 'My name isnt Rusty Russell' outputs 'Hello there!'
+ *     // Given "My name is Rusty" outputs "Hello Rusty!\n"
+ *     // Given "my first name is Rusty Russell" outputs "Hello Rusty Russell!\n"
+ *     // Given "My name isnt Rusty Russell" outputs "Hello there!\n"
  *     int main(int argc, char *argv[])
  *     {
  *             char *person, *input;
index c07e41ed00f0bff432ab35e7e07c05efdc227982..f6a6f0f9e28b2306db1116b8bdefbfc588787809 100644 (file)
@@ -54,8 +54,8 @@
  *                    container_get(&sc), *container_get(&ic) - 1);
  *             return 0;
  *     }
- *     // Given "foo" outputs "Last arg is foo of 1 arguments"
- *     // Given "foo bar" outputs "Last arg is bar of 2 arguments"
+ *     // Given "foo" outputs "Last arg is foo of 1 arguments\n"
+ *     // Given "foo bar" outputs "Last arg is bar of 2 arguments\n"
  *
  * License: CC0 (Public domain)
  *
index 4cc06c270de51749bb4d53ad7eee42b1fc1a9a5f..152606497c221195487380884607b77bf3d94adf 100644 (file)
@@ -1,13 +1,13 @@
 '\" t
 .\"     Title: ccanlint
 .\"    Author: [see the "AUTHOR" section]
-.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: 12/05/2011
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\"      Date: 09/28/2015
 .\"    Manual: \ \&
 .\"    Source: \ \&
 .\"  Language: English
 .\"
-.TH "CCANLINT" "1" "12/05/2011" "\ \&" "\ \&"
+.TH "CCANLINT" "1" "09/28/2015" "\ \&" "\ \&"
 .\" -----------------------------------------------------------------
 .\" * Define some portability stuff
 .\" -----------------------------------------------------------------
@@ -36,7 +36,7 @@ ccanlint \- Make CCAN code modules, and the brightness up\&.
 .sp
 No encoder? No need to \fBccanlint\fR\&. You programmer? Excited to \fBccanlint\fR!
 .sp
-CCAN module is small code of the song\&. \fBccanlint\fR full CCAN testing tool\&. Each test spray bit of wisdom\&. Also score\&. Good score good\&. Bad bad score\&.
+CCAN module is small code of the song\&. \fBccanlint\fR full CCAN testing tool\&. Each test spray bit of wisdom\&. Also score\&. Good score good\&. Bad score bad\&.
 .sp
 \fBccanlint\fR expect the source code in this directory, or command line can be more than one\&. Exit 0 happy if all modules all tests happy\&.
 .SH "OPTIONS"
@@ -73,14 +73,12 @@ Graphviz, then die happy\&.
 .PP
 \fB\-k, \-\-keep\fR
 .RS 4
-
 \fBccanlint\fR
 normally make mess temporary directory, but now it later in forensic\&.
 .RE
 .PP
 \fB\-s, \-\-summary\fR
 .RS 4
-
 \fBccanlint\fR
 just realized there is no message unless you die horrible\&.
 .RE
@@ -107,7 +105,6 @@ Do not run all tests\&. Run this test, and the proof you need\&. Used many times
 .PP
 \fB\-\-compiler\fR=\fICOMPILER\fR
 .RS 4
-
 \fBccanlint\fR
 read config\&.h about finding
 \fICCAN_COMPILER\fR\&. Otherwise use the default when it was built\&. The change, to use this compiler\&.
@@ -148,7 +145,6 @@ question may help to write one\&.
 .PP
 \fBdepends_exist\fR
 .RS 4
-
 \fI_info\fR
 file CCAN other module without saying, must find\&. It is not score 0\&.
 .RE
@@ -197,8 +193,7 @@ unhappy\&.
 \fBhash_if\fR
 .RS 4
 Module wants
-\fBccanlint\fR
-\fIconfig\&.h\fR
+\fBccanlint\fR\fIconfig\&.h\fR
 "#define HAVE_FEATURE" for all feature\&. Function test "#if HAVE_FEATURE" no "#ifdef HAVE_FEATURE" because user might not know about the role at all\&. Intelligent GCC flag
 \fI\-Wundef\fR
 say HAVE_FEATURE not 0, not 1! but only if the use of
@@ -207,7 +202,6 @@ say HAVE_FEATURE not 0, not 1! but only if the use of
 .PP
 \fBinfo_documentation_exists\fR
 .RS 4
-
 \fI_info\fR
 file format is pretty comments\&. Copying someone\&. It is not difficult write documentation!
 .RE
@@ -250,7 +244,6 @@ Hostile to BSD license module, but requires another module of the GPL\&. Perhaps
 .PP
 \fBmain_header_exists\fR
 .RS 4
-
 \fBccanlint\fR
 know the module name directory name\&. Expect the same name for header\&.
 .RE
@@ -283,7 +276,6 @@ Linux kernel programmers more, solve the problem for the space of the final ban
 .PP
 \fBexamples_compile\fR
 .RS 4
-
 \fBccanlint\fR
 very smart! Take
 \fIExample:\fR
@@ -302,15 +294,16 @@ says wow!
 \fBexamples_run\fR
 .RS 4
 If the example program that comments like
-\fI// given foo outputs bar\fR
-\fBccanlint\fR
-will run the food program
+\fI// Given "foo" outputs "bar"\fR\fBccanlint\fR
+will run the program with
 \fIfoo\fR
 in the command line and standard input\&. Happy if
 \fIbar\fR
-are out\&. You can do \*(Aq or " about the production or determined\&. You can also
-\fIoutput contains\fR
-more relaxed controls\&.
+are out\&. If quotes around
+\fIbar\fR
+exact match needed; without quotes whitespace matches any other space and trailing ignored\&. \en is also supported for matching\&. You can also
+\fI"output contains"\fR
+to pass if the output contains the string\&.
 .RE
 .PP
 \fBmodule_links\fR
@@ -364,7 +357,6 @@ Other files
 .PP
 \fBtests_pass\fR
 .RS 4
-
 \fIrun\fR
 and
 \fIapi\fR
@@ -373,7 +365,6 @@ test happy departure\&. If not happy, offer debugger\&.
 .PP
 \fBtests_pass_valgrind\fR
 .RS 4
-
 \fBvalgrind\fR
 the tool of all
 \fIrun\fR
@@ -390,7 +381,6 @@ section, make "tests_pass_valgrind test/TESTNAME:FAIL"\&. If required valgrind a
 .PP
 \fBtests_pass_valgrind_noleaks\fR
 .RS 4
-
 \fBvalgrind\fR
 complain if the memory leak test\&.
 \fI_info\fR
index ea32180917f31c9cd297a07e84b11cb90fd350df..26d46f4c7e1b52274e62c1799003de98c3e0d986 100644 (file)
@@ -175,11 +175,13 @@ test, but happy:
   bad example *ccanlint* says wow!
 
 *examples_run*::
-  If the example program that comments like '// given foo outputs bar'
-  *ccanlint* will run the food program 'foo' in the command line and
-  standard input. Happy if 'bar' are out. You can do ' or " about
-  the production or determined.  You can also 'output contains' more
-  relaxed controls.
+  If the example program that comments like '// [Given "foo"] outputs
+  "bar"' then *ccanlint* will run the program with 'foo' in the
+  command line and standard input.  Happy if 'bar' are out and exit 0.
+  If quotes around 'bar' exact match needed; without quotes whitespace matches
+  any other space and trailing ignored.  \n is also supported for
+  matching.  You can also '"output contains"' to pass if the output
+  contains the string.
 
 *module_links*::
   CCAN link to the program module simply no error.
index 0e473ca4050d142d1845563f3ea9a1e742f37629..3bdb655efcfb2fc3609392c0fef231ec8dff16d9 100644 (file)
@@ -2,6 +2,7 @@
 #include <tools/tools.h>
 #include <ccan/foreach/foreach.h>
 #include <ccan/str/str.h>
+#include <ccan/tal/str/str.h>
 #include <ccan/cast/cast.h>
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -81,146 +82,108 @@ static bool scan_for(const void *ctx, const char *input, const char *fmt, ...)
 }
 
 static char *find_expect(struct ccan_file *file,
-                        char **lines, char **input, bool *exact,
+                        char **lines, char **input,
+                        bool *contains, bool *whitespace, bool *error,
                         unsigned *line)
 {
-       char *expect;
-       const char *fmt;
+       char *rest, *expect;
 
+       *error = false;
        for (; lines[*line]; (*line)++) {
                char *p = lines[*line] + strspn(lines[*line], " \t");
                if (!strstarts(p, "//"))
                        continue;
                p += strspn(p, "/ ");
-               foreach_ptr(fmt,
-                           "given '%s', outputs '%s'",
-                           "given '%s' outputs '%s'",
-                           "given \"%s\", outputs \"%s\"",
-                           "given \"%s\" outputs \"%s\"") {
-                       if (scan_for(file, p, fmt, input, &expect)) {
-                               *exact = true;
-                               return expect;
-                       }
-               }
-
-               foreach_ptr(fmt,
-                           "given '%s', output contains '%s'",
-                           "given '%s' output contains '%s'",
-                           "given \"%s\", output contains \"%s\"",
-                           "given \"%s\" output contains \"%s\"") {
-                       if (scan_for(file, p, fmt, input, &expect)) {
-                               *exact = false;
-                               return expect;
-                       }
-               }
 
-               foreach_ptr(fmt, "outputs '%s'", "outputs \"%s\"") {
-                       if (scan_for(file, p, fmt, &expect)) {
-                               *input = cast_const(char *, "");
-                               *exact = true;
-                               return expect;
+               /* With or without input? */
+               if (strncasecmp(p, "given", strlen("given")) == 0) {
+                       /* Must be of form <given "X"> */
+                       if (!scan_for(file, p, "given \"%s\" %s", input, &p)) {
+                               *error = true;
+                               return p;
                        }
+               } else {
+                       *input = NULL;
                }
 
-               foreach_ptr(fmt,
-                           "given '%s', output contains '%s'",
-                           "given '%s' output contains '%s'",
-                           "given \"%s\", output contains \"%s\"",
-                           "given \"%s\" output contains \"%s\"") {
-                       if (scan_for(file, p, fmt, input, &expect)) {
-                               *exact = false;
-                               return expect;
-                       }
+               if (scan_for(file, p, "outputs \"%s\"", &expect)) {
+                       *whitespace = true;
+                       *contains = false;
+                       return expect;
                }
 
-               /* Unquoted versions... we can get this wrong! */
-               foreach_ptr(fmt,
-                           "given %s, outputs '%s'",
-                           "given '%s', outputs %s",
-                           "given %s, outputs \"%s\"",
-                           "given \"%s\", outputs %s",
-                           "given %s, outputs %s",
-                           "given %s outputs '%s'",
-                           "given '%s' outputs %s",
-                           "given %s outputs \"%s\"",
-                           "given \"%s\" outputs %s",
-                           "given %s outputs %s") {
-                       if (scan_for(file, p, fmt, input, &expect)) {
-                               *exact = true;
-                               return expect;
-                       }
+               if (scan_for(file, p, "output contains \"%s\"", &expect)) {
+                       *whitespace = true;
+                       *contains = true;
+                       return expect;
                }
 
-               foreach_ptr(fmt,
-                           "given %s, output contains '%s'",
-                           "given '%s', output contains %s",
-                           "given %s, output contains \"%s\"",
-                           "given \"%s\", output contains %s",
-                           "given %s, output contains %s",
-                           "given %s output contains '%s'",
-                           "given '%s' output contains %s",
-                           "given %s output contains \"%s\"",
-                           "given \"%s\" output contains %s",
-                           "given %s output contains %s") {
-                       if (scan_for(file, p, fmt, input, &expect)) {
-                               *exact = false;
-                               return expect;
-                       }
+               /* Whitespace-ignoring versions. */
+               if (scan_for(file, p, "outputs %s", &expect)) {
+                       *whitespace = false;
+                       *contains = false;
+                       return expect;
                }
 
-               foreach_ptr(fmt,
-                           "outputs '%s'",
-                           "outputs \"%s\"",
-                           "outputs %s") {
-                       if (scan_for(file, p, fmt, &expect)) {
-                               *input = cast_const(char *, "");
-                               *exact = true;
-                               return expect;
-                       }
+               if (scan_for(file, p, "output contains %s", &expect)) {
+                       *whitespace = false;
+                       *contains = true;
+                       return expect;
                }
 
-               foreach_ptr(fmt,
-                           "output contains '%s'",
-                           "output contains \"%s\"",
-                           "output contains %s") {
-                       if (scan_for(file, p, fmt, &expect)) {
-                               *input = cast_const(char *, "");
-                               *exact = false;
-                               return expect;
-                       }
+               /* Other malformed line? */
+               if (*input || !strncasecmp(p, "output", strlen("output"))) {
+                       *error = true;
+                       return p;
                }
-       }               
+       }
        return NULL;
 }
 
-static char *trim(char *string)
-{
-       while (strends(string, "\n"))
-              string[strlen(string)-1] = '\0';
-       return string;
-}
-
 static char *unexpected(struct ccan_file *i, const char *input,
-                       const char *expect, bool exact)
+                       const char *expect, bool contains, bool whitespace)
 {
        char *output, *cmd;
+       const char *p;
        bool ok;
        unsigned int default_time = default_timeout_ms;
 
-       cmd = tal_fmt(i, "echo '%s' | %s %s",
-                     input, i->compiled[COMPILE_NORMAL], input);
+       if (input)
+               cmd = tal_fmt(i, "echo '%s' | %s %s",
+                             input, i->compiled[COMPILE_NORMAL], input);
+       else
+               cmd = tal_fmt(i, "%s", i->compiled[COMPILE_NORMAL]);
 
        output = run_with_timeout(i, cmd, &ok, &default_time);
        if (!ok)
                return tal_fmt(i, "Exited with non-zero status\n");
 
-       if (exact) {
-               if (streq(output, expect) || streq(trim(output), expect))
+       /* Substitute \n */
+       while ((p = strstr(expect, "\\n")) != NULL) {
+               expect = tal_fmt(cmd, "%.*s\n%s", (int)(p - expect), expect,
+                                p+2);
+       }
+
+       if (!whitespace) {
+               /* Normalize to spaces. */
+               expect = tal_strjoin(cmd,
+                                    tal_strsplit(cmd, expect, " \n\t",
+                                                 STR_NO_EMPTY),
+                                    " ", STR_NO_TRAIL);
+               output = tal_strjoin(cmd,
+                                    tal_strsplit(cmd, output, " \n\t",
+                                                 STR_NO_EMPTY),
+                                    " ", STR_NO_TRAIL);
+       }
+
+       if (contains) {
+               if (strstr(output, expect))
                        return NULL;
        } else {
-               if (strstr(output, expect))
+               if (streq(output, expect))
                        return NULL;
        }
+
        return output;
 }
 
@@ -237,21 +200,32 @@ static void run_examples(struct manifest *m,
                list_for_each(list, i, list) {
                        char **lines, *expect, *input, *output;
                        unsigned int linenum = 0;
-                       bool exact;
+                       bool contains, whitespace, error;
 
                        lines = get_ccan_file_lines(i);
 
-                       for (expect = find_expect(i, lines, &input, &exact,
+                       for (expect = find_expect(i, lines, &input,
+                                                 &contains, &whitespace, &error,
                                                  &linenum);
                             expect;
                             linenum++,
                                     expect = find_expect(i, lines, &input,
-                                                         &exact, &linenum)) {
+                                                         &contains, &whitespace,
+                                                         &error, &linenum)) {
+                               if (error) {
+                                       score_file_error(score, i, linenum+1,
+                                                "Unparsable test line '%s'",
+                                                        lines[linenum]);
+                                       score->pass = false;
+                                       break;
+                               }
+
                                if (i->compiled[COMPILE_NORMAL] == NULL)
                                        continue;
 
                                score->total++;
-                               output = unexpected(i, input, expect, exact);
+                               output = unexpected(i, input, expect,
+                                                   contains, whitespace);
                                if (!output) {
                                        score->score++;
                                        continue;
@@ -259,7 +233,7 @@ static void run_examples(struct manifest *m,
                                score_file_error(score, i, linenum+1,
                                                 "output '%s' didn't %s '%s'\n",
                                                 output,
-                                                exact ? "match" : "contain",
+                                                contains ? "contain" : "match",
                                                 expect);
                                score->pass = false;
                        }