File: testing.scm

package info (click to toggle)
gimp 3.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 222,880 kB
  • sloc: ansic: 870,914; python: 10,965; lisp: 10,857; cpp: 7,355; perl: 4,536; sh: 1,753; xml: 972; yacc: 609; lex: 348; javascript: 150; makefile: 42
file content (484 lines) | stat: -rw-r--r-- 15,323 bytes parent folder | download | duplicates (3)
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
; A testing framework
;
; Independent of GIMP except for gimp_message,
; which you can redefine


; Testing language

; AssertStmt      ~ (assert '(<code>))
; AssertErrorStmt ~ (assert-error '(<code>)  <prefix of expected error string>)
; AssertPDBTrueStmt ~ (assert-PDB-true '(<code>)) !!! Where code is only a call to a PDB returning bool
;                   !!! (assert-PDB-true (not `(code)) is invalid
; AssertPDBFalseStmt ~ (assert-PDB-false '(<code>)) !!! Where code is only a call to a PDB returning bool

; ReportStmt      ~ (testing:report)
; LoadStmt        ~ (testing:load-test <filename>)
; AllPassedPredicate ~ (testing:all-passed?)

; PrintStmt       ~ (test! "message")
;
; AssertStmt and AssertErrorStmt and AssertPDBTrue
;   - have side effects on the testing state,
;   - have side effects on the display, displaying failures.
;   - yield #t or #f meaning pass or fail.

; The other statements yield or display the state.


; Syntax errors

; The test framework WILL NOT handle syntax errors.
; The quoted code under tests must parse without syntax errors.
; Some errors that TinyScheme throws ARE syntax errors, but not named such.
; For example '#\xzzz is a syntax error (z is not a hex digit).
; Thus the test framework won't handle '#\xzzz .


; Algebra of calls
;
; Typically one or more AssertStmt followed by a ReportStmt
; when viewed in the console.
; Or one or more AssertStmt followed by AllPassedPredicate
; to yield an overall testing result,
; when testing is automated.

; Testing error messages
;
; Error messages may have details such as line number of error
; that may change over time.
; Testing expects that details will be a suffix of the error message.
; Passing is measured by comparing given expected prefix of error
; with actual error message.


; Notes on implementation:
;
; Debug stream for testing is gimp-message


; EvalResult object
; is-a tuple (result, error)
; <result> is dynamic type returned by eval
; <error> is type string

(define (make-evalresult result errors)
  ;(gimp-message "make-evalresult")
  (list result errors))
(define (evalresult-get-result x) (car x))
(define (evalresult-get-error x)  (cadr x))
(define (evalresult-has-no-error? x)
   (= (string-length (cadr x)) 0))


;    state

(define testing:passed 0)  ; counter
(define testing:failed '())  ; list

(define (testing:reset!)
  (set! testing:passed 0)
  (set! testing:failed   '()))

(define (testing:log-passed!)
  ; Not announce success to console, but can debug
  (gimp-message "Passed")
  (set! testing:passed (+ testing:passed 1)))

; log any failure
(define (testing:log-fail! failure-string)
  ; Announce fail as it happens
  (displayln "")
  (display "Failed: ")
  (displayln failure-string)
  ; save in state: prepend to list of failures
  (set! testing:failed
        (cons failure-string
              testing:failed)))


(define (testing:log-fail-assert! code eval-result)
  (testing:log-fail! (testing:format-fail-assert code eval-result)))

(define (testing:log-fail-assert-error! code actual-error expected-error)
  (testing:log-fail! (testing:format-fail-assert-error
                 code
                 actual-error
                 expected-error)))

; Print a line break and a string, usually the test name.
; Side effects on the REPL.
; No affect on testing state, no association with any test result
; except by proximity in the output.
(define (test! string)
  (displayln "")
  (displayln string)
  ; also to GIMP Error Console
  (gimp-message string))


; reset testing state when test framework is loaded
(testing:reset!)



;   reporting
; These methods encapsulate formatting of strings and reports

; A report is a summary of counts
; followed by line for each failure
(define (testing:report)
    (testing:display-summary)
    (testing:display-fails))

(define (testing:display-summary)
  (displayln "")
  (display "Passed: ")
  (display testing:passed)
  (display "   Failed: ")
  (displayln (length testing:failed)))

; Display list of failures in time order
; This does not iterate over the list.
; It relies on newlines in the individual failure formats.
(define (testing:display-fails)
  (if (> (length testing:failed) 0)
      (begin
        (displayln "Failures:")
        ; reverse list so it displays in time order
        (display (reverse testing:failed)))
      '()))

; returns a string for failed assert-error
; Of form: Code: foo Actual: bar Expected: zed
(define (testing:format-fail-assert-error code actual-error expected-error)
  (string-append
    "<Code> "
    (any->string code)
    (string #\newline)
    " <Actual> "
    actual-error
    (string #\newline)
    " <Expected> "
    expected-error
    (string #\newline)
    (string #\newline))
)

; returns a string for failed assert
; Of form    Code: foo Error: bar
(define (testing:format-fail-assert code eval-result)
  (string-append
    "<Code>"
    (any->string code)
    (string #\newline)
    ; Any error message.
    "<Error>"
    (evalresult-get-error eval-result)
    (string #\newline)
    (string #\newline)
    ; We don't display result, it must be false
    ; because were given a boolean proposition
    )
)


;   boolean result for entire testing session

(define (testing:all-passed? )
  (not (= (length testing:failed) 0)))



; Record eval-result, a tuple, from eval of code.
; This knows that a passed normal test has true result and empty error.
; <code is> a an object? a Scheme text, is a boolean proposition,
(define (testing:record-assert-result eval-result code)
  ;(gimp-message "record-assert-result")
  ; passed when has no error and result is #t
  (if (and (evalresult-has-no-error? eval-result)
           (evalresult-get-result eval-result))
      (testing:log-passed!)
      ; fail
      (testing:log-fail-assert!
        code
        eval-result)))

; Record eval-result, a tuple, from eval of code.
; This knows that a passed assert-error test has don't care result.
; Instead, this knows the test passes if given <expected-error>
; matches a prefix of the actual error message yielded by eval.
; <result> is dynamic type returned by eval
; <error-message> is type string
; <code> is a an object? a Scheme text, is a boolean proposition.
; <expected-error> is type string
(define (testing:record-assert-error-result eval-result code expected-error)
  ; debug
  ;(displayln "record-assert-error-result")
  ;(displayln eval-result)

  ; expected error string a prefix of actual error string?
  (if (string-prefix?
        expected-error
        (evalresult-get-error eval-result))
      ; passed
      (begin
        (testing:log-passed!)
        #t)
      ; fail, pass asserted code, actual error, expected error
      (begin
        (testing:log-fail-assert-error!
          code
          (evalresult-get-error eval-result)
          expected-error)
        #f)))


(define (testing:record-assert-PDB-truth-result eval-result code ctruth)
  ; ctruth is 0,1
  ; passed when has no error and result is (ctruth)

  ; convert ([0,1]) result to Scheme truth
  (let* ((truth (= (car (evalresult-get-result eval-result))
                  ctruth)))
    (if (and (evalresult-has-no-error? eval-result)
             truth)
        (testing:log-passed!)
        ; fail
        (testing:log-fail-assert!
          code
          eval-result))))

; Strict equality of error strings:
;(if (equal?
;        (evalresult-get-error eval-result)
;        expected-error)


; Statements in the testing DSL.

; The usual or normal test.
; <code> is a boolean proposition expected to yield #t
(define (assert code)
  (let* ((eval-result (harnessed-eval code)))
    ; eval-result is tuple
    ; record normal result i.e. error not expected
    (testing:record-assert-result
      eval-result
      code)
    ; Statements have side-effect on testing state,
    ; but also return boolean result of predicate.
    (evalresult-get-result eval-result )))

; A test of abnormality.
; <code> is not expected to yield any particular value
; <error> is a prefix of error string that <code> is expected to throw.
(define (assert-error code expected-error)
  (let* ((eval-result (harnessed-eval code)))
    ; eval-result is tuple
    ; record normal result i.e. error not expected
    (testing:record-assert-error-result
      eval-result
      code
      expected-error)
    ; Returns whether error matches expected error prefix.
    ))


; The next two functions go away when
; ScriptFu binds more naturally to PDB boolean

; Special test for calls to PDB returning boolean.
; <code> is a call to the PDB expected to yield ([0,1])
; The PDB yields unnatural notion of truth: 0 or 1 wrapped in a list
(define (assert-PDB-true code)
  (let* ((eval-result (harnessed-eval code)))
    ; eval-result is tuple

    ; record normal result i.e. thrown error not expected
    (testing:record-assert-PDB-truth-result
      eval-result
      code
      1)
    ; Statements have side-effect on testing state,
    ; but also return boolean result of predicate.
    (evalresult-get-result eval-result )))

(define (assert-PDB-false code)
  (let* ((eval-result (harnessed-eval code)))
    ; eval-result is tuple

    ; record normal result i.e. thrown error not expected
    (testing:record-assert-PDB-truth-result
      eval-result
      code
      0)
    ; Statements have side-effect on testing state,
    ; but also return boolean result of predicate.
    (evalresult-get-result eval-result )))



; eval code, returning tuple of result and errors
; This knows how to capture errors
; but not what result and errors mean for testing.
; Harnessed means: surrounded by code to capture error messages.
;
; Assert the pre-condition *error-hook* is (throw msg) see script-fu.init.
; So any call (error msg) is (throw msg)
; But we are not using (catch handler code).
; We are only overriding *error-hook*
;
; Any given eval of code under test may yield many calls to the error hook.
; We only record the first error message in an eval of the code under test.

(define (harnessed-eval code)
  ;(gimp-message "harnessed-eval")
  (let* ((old-error-hook *error-hook*) ; save original handler, which is throw
         (errors "") ; initial empty string
         (result #f) ; initial result is #f, not () which is truthy

         (testing-error-hook
           (lambda (xs)
             ;(gimp-message "testing-error-hook")

             ; Only record the first error
             (if (= (string-length errors) 0)
                 (if (string? xs)
                    (begin
                      ;(gimp-message "xs is string")
                      (set! errors xs))
                    (set! errors "Non-string error")))

             ; Do not chain up to old handler: (old-error-hook xs)
             ; Old handler is usually throw, which is error,
             ; and that infinite loops
             ;
             ; This returns to current eval,
             ; which may call this error hook again.
             ;(gimp-message "returning from error hook")
             )))
    ;(gimp-message "override error hook")
    (set! *error-hook* testing-error-hook)
    ;(gimp-message "eval test code")
    (set! result (eval code))
    ;(gimp-message "restore error hook")
    ; restore the error hook for any code in a test script between asserts
    (set! *error-hook* old-error-hook)
    ; return an EvalResult
    (make-evalresult result errors)))



;   string utility


; string-prefix? is in R5RS but not tinyscheme.
; string-prefix? is in various SRFI's but we don't have them here
; So yet again, we need to implement it de novo
(define (string-prefix? str1 str2)
  ; if str1 is longer than str2, it is not a prefix
  (if (> (string-length str1) (string-length str2))
    #f
    ; else str2 is longer str2 than str1.
    ; str1 is a prefix if the leading substring of str2,
    ; that is the length of str1, equals str1.
    (string=?
      str1
      (substring str2 0 (string-length str1)))))



;     filesystem utility

; Return the fullpath of a test script.
; When fileScm is empty, returns path to dir of test scripts.
; From gimp-data-directory i.e. the shared install dir for GIMP
; Require filename is string
; Require suffix, usually ".scm" on the filename

(define (path-to-test-scripts fileScm)
    (let*   (   (path (string-append gimp-data-directory DIR-SEPARATOR "tests")))
        (if (zero? (string-length fileScm)) path (string-append path DIR-SEPARATOR fileScm))))

(define (path-to-test-images fileScm)
    (let*   (   (path (string-append gimp-data-directory DIR-SEPARATOR "images")))
        (if (zero? (string-length fileScm)) path (string-append path DIR-SEPARATOR fileScm))))

; load a test file, which executes it
; Knows where GIMP installs test scripts
;
; Subsequently, testing:report will say results
(define (testing:load-test filename)
  (gimp-message (path-to-test-scripts filename))
  (load (path-to-test-scripts filename)))

; Tell Gimp to load a test image by name
; Returns ID of image
; Knows installed image directory (not dedicated to testing but always there.)
; Accepts image suffixes that Gimp can load.
; Typical is /usr/local/share/gimp/2.99/images/gimp-logo.png
(define (testing:load-test-image filename)
  (gimp-message (path-to-test-images filename))
  ; unpack ID via car
  (car (gimp-file-load RUN-NONINTERACTIVE (path-to-test-images filename))))


; Tell Gimp to load a basic image always distributed with Gimp
; This hides the name of the file.
; Many tests use this, so you can temporarily change the file name
; and many tests will then use a different image.
; But some tests expect the image to have certain properties, like 256x256.
(define (testing:load-test-image-basic)
  (testing:load-test-image "gimp-logo.png"))

; Load a basic image while we are using v3 binding: no car
(define (testing:load-test-image-basic-v3)
  (gimp-file-load RUN-NONINTERACTIVE (path-to-test-images "gimp-logo.png")))

; Returns path to file containing named color profile
; Currently, assumes color profiles downloaded to /work dir.
; FUTURE: platform independent path
; FUTURE: color profile test files in the repo
(define (testing:path-to-color-profile name)
  (string-append "/work/colorProfilesICC/" name))

; Return a new layer in the given image, not inserted.
; The new layer initial attributes are hard-coded.
; The new layer is not added i.e. inserted in the image.
(define (testing:layer-new testImage)
  (gimp-layer-new
    testImage
    "LayerNew"  ; name
    21 22      ; dimensions
    RGB-IMAGE   ; mode
    50.0        ; opacity
    LAYER-MODE-NORMAL))

; Return a new layer in the given image, inserted.
; The new layer initial attributes are hard-coded.
(define (testing:layer-new-inserted testImage)
  (let ((newLayer (testing:layer-new testImage)))
    (gimp-image-insert-layer
            testImage
            newLayer
            0 0) ; parent, position within parent
    newLayer))


; float comparison utility
; are a and b relatively equal, to within epsilon?
(define (equal-relative? a b epsilon)
  (<= (abs (- a b))
      (* epsilon (max (abs a) (abs b)))))


; graphical result utility

; When testing is in the GUI environment and not in batch mode,
; show an image result of testing.
; Now commented out.
; The PDB API has no predicate answering "can open display?"
(define (testing:show image)
  ;(gimp-display-new image)
  )