Read about our upcoming Code of Conduct on this issue

api_test.lua 52.9 KB
Newer Older
1
-- Script API Tests
2
-- Test routines for the scripting API
3
4
-- When adding new Lua APIs, tests for them should go here.

5
require("logger")
6

7
8
9
-- global logger
test_log = nil

10
function printf(s,...)
11
    test_log:writef(s,...)
12
13
14
15

    if not console.visible then
        display.notify_box(s:format(...), 5000)
    end
16
17
end

18
19
20
21
22
23
function alert()
    display.off()
    beep()
    display.on()
end

24
function request_mode(mode, mode_str)
25
    if camera.mode ~= mode or not camera.gui.idle then
26
        printf("Please switch to %s mode.\n", mode_str, mode)
27
28

        while camera.mode ~= mode or not camera.gui.idle do
alex@thinkpad's avatar
alex@thinkpad committed
29
            console.show(); assert(console.visible)
30
            if camera.gui.idle then alert() end
31
            sleep(1)
32
33
        end
    end
34
    sleep(2)
35
36
end

a1ex's avatar
a1ex committed
37
38
39
40
41
function round(x)
    -- https://scriptinghelpers.org/questions/4850/how-do-i-round-numbers-in-lua-answered
    return x + 0.5 - (x + 0.5) % 1
end

42
43
44
45
46
47
48
49
50
51
function print_table(t)
    test_log:write(string.format("%s = ", t))
    local s,e = xpcall(function()
        test_log:serialize(_G[t])
    end, debug.traceback)
    if s == false then
        test_log:write(e)
    end
end

52
53
54
55
-- used for testing Lua 'strict' mode
declared_var = nil

function strict_tests()
56
    printf("Strict mode tests...\n")
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

    -- this should work
    declared_var = 5
    assert(declared_var == 5)

    -- this should raise error
    local s,e = pcall(function() undeclared_var = 7 end)
    assert(s == false)
    assert(e:find("assign to undeclared variable 'undeclared_var'"))

    -- same here
    local s,e = pcall(function() print(undeclared_var) end)
    assert(s == false)
    assert(e:find("variable 'undeclared_var' is not declared"))

72
73
    printf("Strict mode tests passed.\n")
    printf("\n")
74
75
76
end

function generic_tests()
77
    printf("Generic tests...\n")
78
    print_table("arg")
79
80
81
82
83
84
85
86
87
88
89
90
91
92
    print_table("camera")
    print_table("event")
    print_table("console")
    print_table("lv")
    print_table("lens")
    print_table("display")
    print_table("key")
    print_table("menu")
    print_table("movie")
    print_table("dryos")
    print_table("interval")
    print_table("battery")
    print_table("task")
    print_table("property")
93
94
    printf("Generic tests completed.\n")
    printf("\n")
95
96
end

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
function stdio_test()
    local s,e
    
    -- these should print to console
    -- fixme: they should output this:
    -- Hello, stderr. Hello, stdout. 
    -- Hello again, stdout.
    -- Hello again, stderr.
    io.write("Hello, stdout. ")
    io.stderr:write("Hello, stderr. ")
    io.write("\n")

    io.write("Hello again, stdout.\n")
    io.stderr:write("Hello again, stderr.\n")

    -- not implemented
    -- I expected error, but...
    local inp = io.read("*line")
    print("From stdin:", inp)
end

function copy_test(src, dst)
119
    printf("Copy test: %s -> %s\n", src, dst)
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    
    -- will this copy without errors? we'll see
    -- read source file
    local fin = io.open(src, "rb")
    local data = fin:read("*all")
    fin:close()

    -- save a copy
    local fout = io.open(dst, "w")
    fout:write(data)
    fout:close()

    -- verify
    fin = io.open(dst, "rb")
    local check = fin:read("*all")
    local size = fin:seek("end", 0)
    fin:close()
    assert(data == check)
    assert(size == #data)
    
    -- delete the copy
    assert(dryos.remove(dst) == true)
    
    -- check if it was deleted
alex@thinkpad's avatar
alex@thinkpad committed
144
    assert(io.open(dst, "rb") == nil)
145
146
147
148

    -- it should return false this time
    assert(dryos.remove(dst) == false)

149
    printf("Copy test OK\n")
150
151
152
end

function append_test(file)
153
    printf("Append test: %s\n", file)
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

    local data1 = "Allons enfants de la patrie\n"
    local data2 = "Le jour de gloire est arrivé !\n"
    
    -- make sure the file is not there
    dryos.remove(file)
    
    -- create it
    local fout = io.open(file, "a")
    fout:write(data1)
    fout:close()

    -- verify the contents
    local fin = io.open(file, "r")
    local check = fin:read("*all")
    fin:close()
alex@thinkpad's avatar
alex@thinkpad committed
170
    assert(check == data1)
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

    -- reopen it to append something
    fout = io.open(file, "a")
    fout:write(data2)
    fout:close()

    -- verify the contents
    fin = io.open(file, "r")
    check = fin:read("*all")
    fin:close()
    assert(check == data1 .. data2)

    -- delete it
    assert(dryos.remove(file) == true)
    
    -- check if it was deleted
alex@thinkpad's avatar
alex@thinkpad committed
187
    assert(io.open(file, "rb") == nil)
188

189
    printf("Append test OK\n")
190
191
end

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
function rename_test(src, dst)
    printf("Rename test: %s -> %s\n", src, dst)

    local data = "Millions saw the apple fall, " ..
        "but Newton was the one who asked why."
    
    -- create the source file
    local fout = io.open(src, "w")
    fout:write(data)
    fout:close()

    -- verify the contents
    local fin = io.open(src, "r")
    local check = fin:read("*all")
    fin:close()
    assert(check == data)

    -- rename it
    assert(dryos.rename(src, dst) == true)

    -- verify the contents
    fin = io.open(dst, "r")
    check = fin:read("*all")
    fin:close()
    assert(check == data)
    
    -- check if the source file was deleted
    assert(io.open(src, "rb") == nil)

    -- cleanup: delete the renamed file
    assert(dryos.remove(dst) == true)
    
    -- check if it was deleted
    assert(io.open(dst, "rb") == nil)

    printf("Rename test OK\n")
end

230
function test_io()
alex@thinkpad's avatar
alex@thinkpad committed
231
    printf("Testing file I/O...\n")
232
233
234
    stdio_test()
    copy_test("autoexec.bin", "tmp.bin")
    append_test("tmp.txt")
235
236
    rename_test("apple.txt", "banana.txt")
    rename_test("apple.txt", "ML/banana.txt")
alex@thinkpad's avatar
alex@thinkpad committed
237
238
239

    printf("File I/O tests completed.\n")
    printf("\n")
240
241
end

242
243
244
245
246
function test_camera_gui()
    printf("Testing Canon GUI functions...\n")

    camera.gui.play = true
    assert(camera.gui.play == true)
247
    assert(camera.gui.mode == 1)
248
249
250

    -- half-shutter should exit playback mode
    key.press(KEY.HALFSHUTTER)
251
    sleep(1)
252
    assert(camera.gui.play == false)
253
    assert(camera.gui.mode == 0)
254
255
256
257
258
259
260
261
262
263
264
265
266
    key.press(KEY.UNPRESS_HALFSHUTTER)

    -- randomly switch between PLAY, MENU and IDLE (with or without LiveView)
    for i = 1,100 do
        -- we can request MENU or PLAY mode from anywhere
        -- we can only exit these modes if we are already there
        if math.random(1,2) == 1 then
            if math.random(1,2) == 1 then
                printf("Enter PLAY mode...\n");
                camera.gui.play = true
                assert(camera.gui.play == true)
                assert(camera.gui.menu == false)
                assert(camera.gui.idle == false)
267
                assert(camera.gui.mode == 1)
268
269
270
271
272
273
            elseif camera.gui.play then
                printf("Exit PLAY mode...\n");
                camera.gui.play = false
                assert(camera.gui.play == false)
                assert(camera.gui.menu == false)
                assert(camera.gui.idle == true)
274
                assert(camera.gui.mode == 0)
275
276
277
278
279
280
281
282
            end
        else
            if math.random(1,2) == 1 then
                printf("Enter MENU mode...\n");
                camera.gui.menu = true
                assert(camera.gui.menu == true)
                assert(camera.gui.play == false)
                assert(camera.gui.idle == false)
283
                assert(camera.gui.mode == 2)
284
285
286
287
288
289
            elseif camera.gui.menu then
                printf("Exit MENU mode...\n");
                camera.gui.menu = false
                assert(camera.gui.menu == false)
                assert(camera.gui.play == false)
                assert(camera.gui.idle == true)
290
                assert(camera.gui.mode == 0)
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
            end
        end

        -- also play around with LiveView
        if camera.gui.menu == false and camera.gui.play == false then
            if math.random(1,2) == 1 then
                -- do something with LiveView, but not as often as switching MENU/PLAY
                if not lv.enabled then
                    printf("Start LiveView...\n");
                    lv.start()
                elseif lv.paused then
                    printf("Resume LiveView...\n");
                    lv.resume()
                elseif math.random(1,10) < 9 then
                    -- this gets taken less often than the next one, why?
                    -- fixme: biased random?
                    printf("Pause LiveView...\n");
                    lv.pause()
                else
                    printf("Stop LiveView...\n");
                    lv.stop()
                end
            end
        end
    end

    lv.stop()
318
    assert(not lv.enabled)
319
320
321
322
323

    printf("Canon GUI tests completed.\n")
    printf("\n")
end

324
function test_menu()
325
    printf("Testing ML menu API...\n")
326
327
328
329
330
331
332
333

    menu.open()
    assert(menu.select("Expo", "ISO"))

    -- menu values should be set-able as string (displayed value)
    -- or as integer (internal value)
    assert(menu.set("Expo", "ISO", "200"))
    assert(camera.iso.value == 200)
334
    sleep(1)
335
336
337
338

    -- int should be promoted to string because of custom logic in Expo->ISO menu
    assert(menu.set("Expo", "ISO", 1600))
    assert(camera.iso.value == 1600)
339
    sleep(1)
340
341
342
343
344
345
346

    -- move selection (not required for set to work; just for user interface)
    assert(menu.select("Expo", "Picture Style"))

    -- picture styles should be set-able as string
    -- numeric works too, as it sets the internal index
    assert(menu.set("Expo", "Picture Style", "Portrait"))
347
    assert(menu.get("Expo", "Picture Style") == "Portrait")
348
    sleep(1)
349
    assert(menu.set("Expo", "Picture Style", 5)) -- OK, selects Neutral
350
    -- skip string test, as other camera models may use different indices
351
    sleep(1)
352
    assert(menu.set("Expo", "Picture Style", "Landscape"))
353
    assert(menu.get("Expo", "Picture Style") == "Landscape")
354
    sleep(1)
355
    assert(menu.set("Expo", "Picture Style", 1234) == false) -- should fail, out of range
356
    assert(menu.get("Expo", "Picture Style") == "Landscape") -- old selection should not change
357
    sleep(1)
358
359

    assert(menu.select("Overlay"))
360

361
362
    assert(menu.select("Movie"))

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
    -- perform the next test in LiveView
    menu.close()
    lv.start()
    assert(lv.enabled)
    assert(lv.running)
    menu.open()

    assert(menu.select("Movie", "FPS override"))
    assert(menu.set("Movie", "FPS override", "OFF"))            -- "OFF" and "ON" are boolean matches for zero/nonzero internal state
    assert(menu.get("Movie", "FPS override") == "OFF")
    assert(menu.set("FPS override", "Desired FPS", "23.976"))   -- this menu entry will print "23.976 (from 25)" or something like that
    assert(menu.get("FPS override", "Desired FPS"):sub(1,13) == "23.976 (from ")
    assert(menu.set("FPS override", "Desired FPS", "5"))        -- this menu entry will print "5 (from 30)" or something like that
    assert(menu.get("FPS override", "Desired FPS"):sub(1,8) == "5 (from ")
    assert(menu.set("FPS override", "Desired FPS", "10"))       -- this menu entry will print "10 (from 25)" or something like that
    assert(menu.get("FPS override", "Desired FPS"):sub(1,9) == "10 (from ")
    assert(menu.set("FPS override", "Optimize for", "Exact FPS")) -- nothing fancy here, just request exact frame rate
    assert(menu.get("FPS override", "Optimize for") == "Exact FPS")
    assert(menu.set("Movie", "FPS override", "ON"))             -- enable FPS override
    assert(menu.get("Movie", "FPS override") ~= "ON")           -- the menu entry will print something else
    sleep(2)                                                    -- switching the frame rate takes a while
    assert(menu.get("Movie", "FPS override") == "10.000")       -- it should eventually settle to our requested value
    assert(menu.get("FPS override", "Actual FPS") == "10.000")  -- current FPS value can be read from here
    assert(menu.set("Movie", "FPS override", "OFF"))            -- that was it
    assert(menu.get("Movie", "FPS override") == "OFF")          -- make sure it's turned off

    -- LiveView test completed
    menu.close()
    lv.stop()
    assert(not lv.running)
    assert(not lv.enabled)
    menu.open()

    assert(menu.select("Shoot"))
397
398
399
400
    assert(menu.select("Shoot", "Advanced Bracket"))

    -- boolean items should be set-able as int (0 or 1)
    assert(menu.set("Shoot", "Advanced Bracket", 1))
401
    assert(menu.get("Shoot", "Advanced Bracket", 0) == 1)
402
    sleep(1)
403
404
405
406

    -- or as string (if the user interface displays "ON" or "OFF")
    -- here, actual string will be "ON, 10s" or similar (also accepted)
    assert(menu.set("Shoot", "Intervalometer", "ON"))
407
    assert(menu.get("Shoot", "Intervalometer", 0) == 1)
408
    sleep(1)
409
410
411

    -- turning off should be straightforward
    assert(menu.set("Shoot", "Advanced Bracket", "OFF"))
412
413
    assert(menu.get("Shoot", "Advanced Bracket", 0) == 0)
    assert(menu.get("Shoot", "Advanced Bracket") == "OFF")
414
    sleep(1)
415

416
417
418
419
420
421
422
423
    -- "ON" and "OFF" are interpreted as booleans,
    -- even if the menu entry doesn't display exactly this string
    assert(menu.set("Shoot", "Advanced Bracket", "ON"))
    assert(menu.get("Shoot", "Advanced Bracket") ~= "ON")
    assert(menu.get("Shoot", "Advanced Bracket") ~= "OFF")
    assert(menu.set("Shoot", "Advanced Bracket", "OFF"))
    assert(menu.get("Shoot", "Advanced Bracket") == "OFF")

424
    assert(menu.set("Shoot", "Intervalometer", 0))
425
426
    assert(menu.get("Shoot", "Intervalometer", 0) == 0)
    assert(menu.get("Shoot", "Intervalometer") == "OFF")
427
    sleep(1)
428
429
430

    -- move to Intervalometer menu
    assert(menu.select("Shoot", "Intervalometer"))
431
    sleep(1)
432
433

    -- enter the submenu
434
    assert(menu.select("Intervalometer", "Take a pic every"))
435
    sleep(1)
436
437
438

    -- note: setting menu by string works by brute force
    -- that is, trying every possible value and comparing the string
439
440
441
442
443
    assert(menu.set("Intervalometer", "Take a pic every", "1m10s"))
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")

    -- going from 1m10s to 1m30s should be easy
444
    assert(menu.set("Intervalometer", "Take a pic every", "1m30s"))
445
446
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 90)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m30s")
447
448
449
450
451

    -- going back to 1m10s would have to go through LOTS of intermediate values
    assert(menu.set("Intervalometer", "Take a pic every", "1m10s"))
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")
452
    sleep(1)
453
454
455

    -- actual string will be 10s
    assert(menu.set("Intervalometer", "Take a pic every", "10"))
456
457
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 10)
    assert(menu.get("Intervalometer", "Take a pic every") == "10s")
458
    sleep(1)
459

460
461
    -- integer should work as well, as long as the internal state variable matches the menu value
    -- on other menus, where internal state is some index, integer argument may not be the best choice
462
    assert(menu.set("Intervalometer", "Take a pic every", 70))
463
464
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")
465
    sleep(1)
466
467

    -- out of range, should fail
468
    assert(menu.set("Intervalometer", "Take a pic every", 7000000) == false)
469
470
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")
471
    sleep(1)
472
473

    -- exit submenu
474
    assert(menu.select("Shoot", "Intervalometer"))
475
    sleep(1)
476

477
478
    -- let's try some more submenus
    -- todo: more multi-level submenus
479
480
481
482
483
484
485
486
487
488
    assert(menu.select("Advanced Bracket", "Frames")); sleep(1)
    assert(menu.select("Advanced Bracket", "Sequence")); sleep(1)
    assert(menu.select("Advanced Bracket", "ISO shifting")); sleep(1)
    assert(menu.select("Bulb Timer", "Exposure duration")); sleep(1)
    assert(menu.select("Shoot Preferences", "Snap Simulation")); sleep(1)
    assert(menu.select("Misc key settings", "Sticky HalfShutter")); sleep(1)
    assert(menu.select("Play mode actions", "Trigger key(s)")); sleep(1)
    assert(menu.select("LiveView zoom tweaks", "Zoom on HalfShutter")); sleep(1)
    assert(menu.select("Lens info", "Lens ID")); sleep(1)
    assert(menu.select("Shoot", "Intervalometer")); sleep(1)
489

490
491
492
    -- selection in the Modified menu
    -- note: we have already modified "Take a pic every"
    -- so at least this setting should appear there
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
    assert(menu.set("Shoot", "Intervalometer", "ON")); sleep(1)
    assert(menu.select("Modified", "Intervalometer")); sleep(1)             -- we should find Intervalometer in the Modified menu
    assert(menu.select("Modified", "Take a pic every")); sleep(1)           -- same for "Take pic every" (smaller font)
    assert(menu.select("Modified", "Intervalometer")); sleep(1)             -- scroll back to intervalometer
    assert(menu.set("Shoot", "Intervalometer", "OFF")); sleep(1)            -- turn it off
    assert(menu.select("Modified", "Take a pic every") == false); sleep(1)  -- now "Take a pic every" should disappear (submenus are only expanded when the main entry is on)
    assert(menu.set("Shoot", "Intervalometer", "ON")); sleep(1)             -- re-enable intervalometer
    assert(menu.select("Modified", "Take a pic every")); sleep(1)           -- now "Take a pic every" should re-appear
    assert(menu.set("Intervalometer", "Take a pic every", 10)); sleep(1)    -- set "Take a pic every" back to default
    assert(menu.select("Modified", "Intervalometer")); sleep(1)             -- scroll back to intervalometer
    assert(menu.set("Shoot", "Intervalometer", "OFF")); sleep(1)            -- turn it off
    assert(menu.select("Modified", "Take a pic every") == false); sleep(1)  -- now "Take a pic every" should disappear
    assert(menu.select("Shoot", "Intervalometer")); sleep(1)                -- move back to Shoot -> Intervalometer
    assert(menu.select("Intervalometer", "Take a pic every")); sleep(1)     -- set other intervalometer settings to default (just in case)
    assert(menu.select("Intervalometer", "Start trigger")); sleep(1)        -- the menu.select calls are just for visual effect
    assert(menu.set("Intervalometer", "Start trigger", "Leave Menu")); sleep(0.5);
    assert(menu.select("Intervalometer", "Start after")); sleep(0.5)
    assert(menu.set("Intervalometer", "Start after", "3s")); sleep(0.5);
    assert(menu.select("Intervalometer", "Stop after")); sleep(0.5)
    assert(menu.set("Intervalometer", "Stop after", "Disabled")); sleep(0.5);
    assert(menu.select("Shoot", "Intervalometer")); sleep(1)
    assert(menu.select("Modified", "Intervalometer") == false); sleep(1)    -- now Modified -> Intervalometer should disappear
515

516
    -- non-existent menus; should fail
517
518
519
520
    assert(menu.select("Dinosaur") == false)
    assert(menu.select("Shoot", "Crocodile") == false)

    -- menu.get/set return nil if the menu was not found
521
522
    assert(menu.get("Shoot", "Introvolometer", 0) == nil)
    assert(menu.get("Shoot", "Brack") == nil)
523
524
    assert(menu.set("Shoot", "Introvolometer", 1) == nil)
    assert(menu.set("Shoot", "Introvolometer", "OFF") == nil)
525
526
527
528
529
530

    menu.close()

    -- exercise the menu backend a bit
    for i = 1,5 do
        menu.open()
531
        assert(camera.gui.idle == false)
532
        menu.close()
533
        assert(camera.gui.idle == true)
534
535
536
537
538
539
    end

    printf("Menu tests completed.\n")
    printf("\n")
end

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
function taskA()
    printf("Task A started.\n")
    local i
    for i = 1,100 do
        printf("Task A: %d\n", i)
        task.yield(math.random(10,50))
    end
end

function taskB()
    printf("Task B started.\n")
    local i
    for i = 1,100 do
        printf("Task B: %d\n", i)
        task.yield(math.random(10,50))
    end
end

558
559
560
561
562
563
564
565
566
567
568
569
function taskC()
    printf("Task C started.\n")
    msleep(math.random(10,50))
    printf("Task C finished.\n")
end

function taskD()
    io.write("Task D started.\n")
    msleep(math.random(10,50))
    io.write("Task D finished.\n")
end

570
571
572
function test_multitasking()
    printf("Testing multitasking...\n")

573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
    -- FIXME: this fails
    --task.create(taskA)
    --task.create(taskB)
    --task.yield(5000)
    
    printf("Only one task allowed to interrupt...\n")

    -- small test with logging to file
    for i = 1,10 do
        task.create(taskC)
        printf("Main task yielding.\n")
        task.yield(math.random(10,50))
        printf("Main task back.\n")
    end

    -- larger test with logging to console only
    for i = 1,1000 do
        task.create(taskD)
        io.write("Main task yielding.\n")
        task.yield(math.random(10,50))
        io.write("Main task back.\n")
    end

    task.yield(500)
597
598
599
600
601

    printf("Multitasking tests completed.\n")
    printf("\n")
end

602
603
function test_keys()
    printf("Testing half-shutter...\n")
604
605
    for i = 1,10 do
        -- open Canon menu
606
        camera.gui.menu = true
607
        sleep(1)
608
609
        assert(camera.gui.menu == true)
        assert(camera.gui.idle == false)
610
        key.press(KEY.HALFSHUTTER)
611
        sleep(0.2)
612
613
614
        if key.last ~= KEY.HALFSHUTTER then
            printf("warning: last key not half-shutter, but %d\n", key.last)
        end
615
        sleep(1)
616
        -- half-shutter should close Canon menu
617
618
        assert(camera.gui.menu == false)
        assert(camera.gui.idle == true)
619
        key.press(KEY.UNPRESS_HALFSHUTTER)
620
        sleep(0.2)
621
622
623
        if key.last ~= KEY.UNPRESS_HALFSHUTTER then
            printf("warning: last key not unpress half-shutter, but %d\n", key.last)
        end
624
    end
625
626
627
    printf("Half-shutter test OK.\n")
    
    -- todo: test other key codes? press/unpress events?
alex@thinkpad's avatar
alex@thinkpad committed
628
    printf("\n")
629
630
end

631
function test_camera_exposure()
alex@thinkpad's avatar
alex@thinkpad committed
632
    printf("Testing exposure settings...\n")
633
634
635
636
637
638
    printf("Camera    : %s (%s) %s\n", camera.model, camera.model_short, camera.firmware)
    printf("Lens      : %s\n", lens.name)
    printf("Shoot mode: %s\n", camera.mode)
    printf("Shutter   : %s (raw %s, %ss, %sms, apex %s)\n", camera.shutter, camera.shutter.raw, camera.shutter.value, camera.shutter.ms, camera.shutter.apex)
    printf("Aperture  : %s (raw %s, f/%s, apex %s)\n", camera.aperture, camera.aperture.raw, camera.aperture.value, camera.aperture.apex)
    printf("Av range  : %s..%s (raw %s..%s, f/%s..f/%s, apex %s..%s)\n",
639
640
641
642
643
        camera.aperture.min, camera.aperture.max,
        camera.aperture.min.raw, camera.aperture.max.raw,
        camera.aperture.min.value, camera.aperture.max.value,
        camera.aperture.min.apex, camera.aperture.max.apex
    )
644
645
646
    printf("ISO       : %s (raw %s, %s, apex %s)\n", camera.iso, camera.iso.raw, camera.iso.value, camera.iso.apex)
    printf("EC        : %s (raw %s, %s EV)\n", camera.ec, camera.ec.raw, camera.ec.value)
    printf("Flash EC  : %s (raw %s, %s EV)\n", camera.flash_ec, camera.flash_ec.raw, camera.flash_ec.value)
647

648
    request_mode(MODE.M, "M")
649
    local old_value = camera.shutter.raw
650
    printf("Setting shutter to random values...\n")
651
    for k = 1,100 do
652
653
        local method = math.random(1,4)
        local d = nil
654
655
        if method == 1 then
            local s = math.random(1,30)
656
657
658
659
660
            if math.random(1,2) == 1 then
                camera.shutter.value = s
            else
                camera.shutter = s
            end
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
            d = math.abs(math.log(camera.shutter.value,2) - math.log(s,2))
        elseif method == 2 then
            local ms = math.random(1,30000)
            camera.shutter.ms = ms
            d = math.abs(math.log(camera.shutter.ms,2) - math.log(ms,2))
        elseif method == 3 then
            local apex = math.random(-5*100,12*100)/100
            camera.shutter.apex = apex
            d = math.abs(camera.shutter.apex - apex)
        elseif method == 4 then
            local raw = math.random(16,152)
            camera.shutter.raw = raw
            d = math.abs(camera.shutter.raw - raw) / 8
        end

        -- difference between requested and actual shutter speed should be max 1.5/8 EV
        -- (most cameras accept 0, 3/8, 4/8, 5/8, 1 EV, the largest gap being 3/8 EV,
        --  and you should always get the nearest valid shutter speed)
        if d > 1.5/8 + 1e-3 then
680
            printf("Error: shutter delta %s EV\n", d)
681
682
683
684
        end

        -- seconds and ms fields should be consistent
        if math.abs(camera.shutter.value - camera.shutter.ms/1000) > 1e-3 then
685
            printf("Error: shutter %ss != %sms\n", camera.shutter.value, camera.shutter.ms)
686
687
688
        end
        
        -- seconds and apex should be consistent
689
        -- see http://dougkerr.net/Pumpkin/articles/APEX.pdf
690
        local expected_apex = math.log(1/camera.shutter.value,2)
691
        if math.abs(expected_apex - camera.shutter.apex) > 0.01 then
692
            printf("Error: shutter %ss != Tv%s, expected %s\n", camera.shutter.value, camera.shutter.apex, expected_apex)
693
694
695
696
697
        end
        
        -- setting shutter to the same value, using any method (s,ms,apex,raw)
        -- should not change anything
        for i,field in pairs{"value","ms","apex","raw"} do
698
699
700
701
702
            local current = {}
            current.apex  = camera.shutter.apex
            current.value = camera.shutter.value
            current.ms    = camera.shutter.ms
            current.raw   = camera.shutter.raw
703
704
705
            
            -- ms is integer, so tests at very low values will fail because of roundoff errors
            -- however, if we have set the shutter in ms from the beginning, it will work fine
706
            if field == "ms" and current.ms < 10 and method ~= 2 then
707
708
709
710
                -- how do you write "continue" in Lua?!
                field = "value"
            end
            
711
            camera.shutter[field] = current[field]
712
            
713
            if camera.shutter.value ~= current.value then
714
                printf("Error: shutter set to %s=%s, got %ss, expected %ss\n", field, current[field], camera.shutter.value, current.value)
715
            end
716
            if camera.shutter.ms ~= current.ms then
717
                printf("Error: shutter set to %s=%s, got %sms, expected %sms\n", field, current[field], camera.shutter.ms, current.ms)
718
            end
719
            if camera.shutter.apex ~= current.apex then
720
                printf("Error: shutter set to %s=%s, got Tv%s, expected Tv%s\n", field, current[field], camera.shutter.apex, current.apex)
721
            end
722
            if camera.shutter.raw ~= current.raw then
723
                printf("Error: shutter set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.shutter.raw, current.raw)
724
725
726
            end
        end
    end
727
    camera.shutter.raw = old_value
728

729
    request_mode(MODE.M, "M")
730
    old_value = camera.iso.raw
731
    printf("Setting ISO to random values...\n")
732
    for k = 1,100 do
733
734
        local method = math.random(1,3)
        local d = nil
735
        -- 500D: max ISO 3200
736
        if method == 1 then
737
            local iso = math.random(100, 3200)
738
739
740
741
742
            if math.random(1,2) == 1 then
                camera.iso.value = iso
            else
                camera.iso = iso
            end
743
744
            d = math.abs(math.log(camera.iso.value,2) - math.log(iso,2))
        elseif method == 2 then
745
            local apex = math.random(5*100,10*100)/100
746
747
748
            camera.iso.apex = apex
            d = math.abs(camera.iso.apex - apex)
        elseif method == 3 then
749
            local raw = math.random(72, 112)
750
751
752
753
754
755
            camera.iso.raw = raw
            d = math.abs(camera.iso.raw - raw) / 8
        end

        -- difference between requested and actual ISO should be max 1/3 EV
        if d > 1/3 then
756
            printf("Error: ISO delta %s EV\n", d)
757
758
759
        end

        -- ISO and Sv (APEX) should be consistent
760
        local expected_apex = math.log(camera.iso.value/3.125, 2)
761
        if math.abs(expected_apex - camera.iso.apex) > 0.2 then
762
            printf("Error: ISO %s != Sv%s, expected %s\n", camera.iso.value, camera.iso.apex, expected_apex)
763
764
765
766
767
        end

        -- setting ISO to the same value, using any method (value,apex,raw)
        -- should not change anything
        for i,field in pairs{"value","apex","raw"} do
768
769
770
771
            local current = {}
            current.apex  = camera.iso.apex
            current.value = camera.iso.value
            current.raw   = camera.iso.raw
772
            
773
            camera.iso[field] = current[field]
774
            
775
            if camera.iso.value ~= current.value then
776
                printf("Error: ISO set to %s=%s, got %s, expected %s\n", field, current[field], camera.iso.value, current.value)
777
            end
778
            if camera.iso.apex ~= current.apex then
779
                printf("Error: ISO set to %s=%s, got Sv%s, expected Sv%s\n", field, current[field], camera.iso.apex, current.apex)
780
            end
781
            if camera.iso.raw ~= current.raw then
782
                printf("Error: ISO set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.iso.raw, current.raw)
783
784
785
            end
        end
    end
786
    camera.iso.raw = old_value
787

a1ex's avatar
a1ex committed
788
    if camera.aperture.min.raw == camera.aperture.max.raw then
789
        printf("This lens does not have variable aperture (skipping test).\n")
a1ex's avatar
a1ex committed
790
791
    else
        request_mode(MODE.M, "M")
792
        old_value = camera.aperture.raw
793
        printf("Setting aperture to random values...\n")
a1ex's avatar
a1ex committed
794
        for k = 1,100 do
795
796
797
            local method = math.random(1,3)
            local d = nil
            local extra_tol = 0
a1ex's avatar
a1ex committed
798
799
            if method == 1 then
                local av = math.random(round(camera.aperture.min.value*10), round(camera.aperture.max.value*10)) / 10
800
801
802
803
804
                if math.random(1,2) == 1 then
                    camera.aperture.value = av
                else
                    camera.aperture = av
                end
a1ex's avatar
a1ex committed
805
                d = math.abs(math.log(camera.aperture.value,2) - math.log(av,2)) * 2
806
807
                -- when checking the result, allow a larger difference (0.1 units) - see note below
                extra_tol = math.abs(math.log(av,2) - math.log(av-0.1,2)) * 2
a1ex's avatar
a1ex committed
808
809
810
811
812
813
814
815
816
817
818
            elseif method == 2 then
                local apex = math.random(round(camera.aperture.min.apex*100), round(camera.aperture.max.apex*100)) / 100
                camera.aperture.apex = apex
                d = math.abs(camera.aperture.apex - apex)
            elseif method == 3 then
                local raw = math.random(camera.aperture.min.raw, camera.aperture.max.raw)
                camera.aperture.raw = raw
                d = math.abs(camera.aperture.raw - raw) / 8
            end

            -- difference between requested and actual aperture should be max 1.5/8 EV
819
820
821
            -- note: when using F-numbers, the difference may be larger, because of the rounding done
            -- to match Canon values (e.g. raw 48 would be f/5.66 (f/5.7), instead of Canon's f/5.6)
            if (d > 1.5/8 + extra_tol) then
alex@thinkpad's avatar
alex@thinkpad committed
822
                printf("Error: aperture delta %s EV (expected < %s, %s, method=%d)\n", d, 1.5/8 + extra_tol, camera.aperture, method)
a1ex's avatar
a1ex committed
823
824
825
            end

            -- aperture and Av (APEX) should be consistent
826
            local expected_apex = math.log(camera.aperture.value, 2) * 2
a1ex's avatar
a1ex committed
827
            if math.abs(expected_apex - camera.aperture.apex) > 0.2 then
828
                printf("Error: aperture %s != Av%s, expected %s\n", camera.aperture.value, camera.aperture.apex, expected_apex)
a1ex's avatar
a1ex committed
829
830
831
832
833
            end

            -- setting aperture to the same value, using any method (value,apex,raw)
            -- should not change anything
            for i,field in pairs{"value","apex","raw"} do
834
835
836
837
                local current = {}
                current.apex  = camera.aperture.apex
                current.value = camera.aperture.value
                current.raw   = camera.aperture.raw
a1ex's avatar
a1ex committed
838
                
839
                camera.aperture[field] = current[field]
a1ex's avatar
a1ex committed
840
                
841
                if camera.aperture.value ~= current.value then
842
                    printf("Error: aperture set to %s=%s, got %s, expected %s\n", field, current[field], camera.aperture.value, current.value)
a1ex's avatar
a1ex committed
843
                end
844
                if camera.aperture.apex ~= current.apex then
845
                    printf("Error: aperture set to %s=%s, got Sv%s, expected Sv%s\n", field, current[field], camera.aperture.apex, current.apex)
a1ex's avatar
a1ex committed
846
                end
847
                if camera.aperture.raw ~= current.raw then
848
                    printf("Error: aperture set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.aperture.raw, current.raw)
a1ex's avatar
a1ex committed
849
850
851
                end
            end
        end
852
        camera.aperture.raw = old_value
a1ex's avatar
a1ex committed
853
854
    end

855
    request_mode(MODE.AV, "Av")
856
    old_value = camera.ec.raw
857
    printf("Setting EC to random values...\n")
858
    for k = 1,100 do
859
860
        local method = math.random(1,2)
        local d = nil
861
862
        if method == 1 then
            local ec = math.random(-2*100, 2*100) / 100
863
864
865
866
867
            if math.random(1,2) == 1 then
                camera.ec.value = ec
            else
                camera.ec = ec
            end
868
869
870
871
872
873
874
875
876
            d = math.abs(camera.ec.value - ec,2)
        elseif method == 2 then
            local raw = math.random(-16, 16)
            camera.ec.raw = raw
            d = math.abs(camera.ec.raw - raw) / 8
        end

        -- difference between requested and actual EC should be max 1.5/8 EV
        if d > 1.5/8 then
877
            printf("Error: EC delta %s EV\n", d)
878
879
880
        end

        -- EC and raw should be consistent
881
        local expected_ec = camera.ec.raw / 8
882
        if math.abs(expected_ec - camera.ec.value) > 0.001 then
883
            printf("Error: EC raw %s != %s EV, expected %s EV\n", camera.ec.raw, camera.ec.value, expected_ec)
884
885
886
887
888
        end

        -- setting EC to the same value, using any method (value,raw)
        -- should not change anything
        for i,field in pairs{"value","raw"} do
889
890
891
            local current = {}
            current.value = camera.ec.value
            current.raw   = camera.ec.raw
892
            
893
            camera.ec[field] = current[field]
894
            
895
            if camera.ec.value ~= current.value then
896
                printf("Error: EC set to %s=%s, got %s, expected %s EV\n", field, current[field], camera.ec.value, current.value)
897
            end
898
            if camera.ec.raw ~= current.raw then
899
                printf("Error: EC set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.ec.raw, current.raw)
900
901
902
            end
        end
    end
903
    camera.ec.raw = old_value
904
905

    -- copy/paste & replace from EC (those two should behave in the same way)
906
    old_value = camera.flash_ec.raw
907
    printf("Setting Flash EC to random values...\n")
908
    for k = 1,100 do
909
910
        local method = math.random(1,2)
        local d = nil
911
912
        if method == 1 then
            local fec = math.random(-2*100, 2*100) / 100
913
914
915
916
917
            if math.random(1,2) == 1 then
                camera.flash_ec.value = fec
            else
                camera.flash_ec = fec
            end
918
919
920
921
922
923
924
925
926
            d = math.abs(camera.flash_ec.value - fec,2)
        elseif method == 2 then
            local raw = math.random(-16, 16)
            camera.flash_ec.raw = raw
            d = math.abs(camera.flash_ec.raw - raw) / 8
        end

        -- difference between requested and actual EC should be max 1.5/8 EV
        if d > 1.5/8 then
927
            printf("Error: FEC delta %s EV\n", d)
928
929
930
        end

        -- EC and raw should be consistent
931
        local expected_fec = camera.flash_ec.raw / 8
932
        if math.abs(expected_fec - camera.flash_ec.value) > 0.001 then
933
            printf("Error: FEC raw %s != %s EV, expected %s EV\n", camera.flash_ec.raw, camera.flash_ec.value, current.expected_fec)
934
935
936
937
938
        end

        -- setting EC to the same value, using any method (value,raw)
        -- should not change anything
        for i,field in pairs{"value","raw"} do
939
940
941
            local current = {}
            current.value = camera.flash_ec.value
            current.raw   = camera.flash_ec.raw
942
            
943
            camera.flash_ec[field] = current[field]
944
            
945
            if camera.flash_ec.value ~= current.value then
946
                printf("Error: FEC set to %s=%s, got %s, expected %s EV\n", field, current[field], camera.flash_ec.value, current.value)
947
            end
948
            if camera.flash_ec.raw ~= current.raw then
949
                printf("Error: FEC set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.flash_ec.raw, current.raw)
950
951
952
            end
        end
    end
953
    camera.flash_ec.raw = old_value
954

955
956
    printf("Exposure tests completed.\n")
    printf("\n")
957
958
end

959
960
961
962
963
964
965
966
967
968
969
970
function print_file_size(filename)
    local f = io.open(filename, "rb")
    if f then
        local size = f:seek("end", 0)
        f:close()
        printf("%s: %s\n", filename, size);
        return size
    else
        printf("%s not found.\n", filename);
    end
end

971
function test_camera_take_pics()
alex@thinkpad's avatar
alex@thinkpad committed
972
    printf("Testing picture taking functions...\n")
973
974
975

    request_mode(MODE.M, "M")
    camera.shutter = 1/50
976
    sleep(2)
977
978
979
    
    printf("Snap simulation test...\n")
    assert(menu.set("Shoot Preferences", "Snap Simulation", 1))
980
    local initial_file_num = dryos.shooting_card.file_number
981
982
983
984
    camera.shoot()
    assert(dryos.shooting_card.file_number == initial_file_num)
    assert(menu.set("Shoot Preferences", "Snap Simulation", 0))

985
    sleep(2)
986
987

    printf("Single picture...\n")
988
    -- let's also check if we can find the image file
989
    initial_file_num = dryos.shooting_card.file_number
990
991
992
993
994
995
996
997
    local image_path_cr2 = dryos.shooting_card:image_path(1, ".CR2") -- next image (assume CR2)
    local image_path_jpg = dryos.shooting_card:image_path(1, ".JPG") -- next image (assume JPG)
    local image_path_auto = dryos.shooting_card:image_path(1)        -- next image (autodetect extension)
    assert(dryos.shooting_card:image_path(1, nil) == image_path_auto)
    assert(image_path_auto == image_path_cr2 or image_path_auto == image_path_jpg)
    assert(dryos.shooting_card:image_path(1, "") .. ".CR2" == image_path_cr2)
    assert(dryos.shooting_card:image_path(1, "") .. ".JPG" == image_path_jpg)

998
999
1000
1001
    -- the image file(s) should not be present before taking the picture :)
    assert(io.open(image_path_cr2, "rb") == nil)
    assert(io.open(image_path_jpg, "rb") == nil)

1002
    camera.shoot()
1003

1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
    assert(dryos.shooting_card:image_path(0) == image_path_auto) -- last captured image
    assert(dryos.shooting_card:image_path() == image_path_auto)  -- that's the default

    -- manually build last image path name
    -- next image path is harder to build manually, as you need to take care
    -- of wrapping around (100CANON/IMG_9999.CR2 -> 101CANON/IMG_0001.CR2)
    -- this is why dryos.shooting_card:image_path is preferred -- it handles these edge cases for you
    local image_path_dcim = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", dryos.shooting_card.file_number)
    assert(image_path_dcim == dryos.shooting_card:image_path(0, ""))

1014
    -- but either CR2 or JPG should be there afterwards (or maybe both)
1015
    assert((dryos.shooting_card.file_number - initial_file_num) % 9999 == 1)
1016
1017
1018
1019
1020
    camera.wait()
    local size_cr2 = print_file_size(image_path_cr2)
    local size_jpg = print_file_size(image_path_jpg)
    assert(size_cr2 or size_jpg)

1021
1022
1023
1024
1025
1026
1027
    -- let's review it
    assert(camera.gui.qr or camera.gui.idle)
    camera.gui.play = true
    assert(camera.gui.play == true)
    assert(camera.gui.play_photo == true)
    assert(camera.gui.play_movie == false)

1028
    sleep(2)
1029
1030
1031
1032

    printf("Two burst pictures...\n")
    printf("Ideally, the camera should be in some continuous shooting mode (not checked).\n")
    initial_file_num = dryos.shooting_card.file_number
1033
1034
1035
1036
1037

    -- we should also be able to change the file prefix
    local old_prefix = dryos.image_prefix
    dryos.image_prefix = "ABC_"
    assert(dryos.image_prefix == "ABC_")
1038
1039
1040
1041
1042
1043
    -- edge case if image number is 9997 (next image 9998) before running api_test.lua
    -- (image1 will be 100CANON/IMG_9999 and image2 should be 101CANON/IMG_0001)
    local image1_path_cr2 = dryos.shooting_card:image_path(1, ".CR2")
    local image1_path_jpg = dryos.shooting_card:image_path(1, ".JPG")
    local image2_path_cr2 = dryos.shooting_card:image_path(2, ".CR2")
    local image2_path_jpg = dryos.shooting_card:image_path(2, ".JPG")
1044
1045
1046
1047
1048
    assert(io.open(image1_path_cr2, "rb") == nil)
    assert(io.open(image1_path_jpg, "rb") == nil)
    assert(io.open(image2_path_cr2, "rb") == nil)
    assert(io.open(image2_path_jpg, "rb") == nil)

1049
    camera.burst(2)
1050

1051
    assert((dryos.shooting_card.file_number - initial_file_num) % 9999 == 2)
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
    camera.wait()
    local size1_cr2 = print_file_size(image1_path_cr2)
    local size1_jpg = print_file_size(image1_path_jpg)
    local size2_cr2 = print_file_size(image2_path_cr2)
    local size2_jpg = print_file_size(image2_path_jpg)
    assert(size1_cr2 or size1_jpg)
    assert(size2_cr2 or size2_jpg)
    dryos.image_prefix = ""     -- empty string restores the old prefix
    assert(dryos.image_prefix == old_prefix)

    -- let's try some more
    dryos.image_prefix = "XYZ_"
    assert(dryos.image_prefix == "XYZ_")
    dryos.image_prefix = "1234"
    assert(dryos.image_prefix == "1234")
    dryos.image_prefix = "$#@!"
    assert(dryos.image_prefix == "$#@!")
    dryos.image_prefix = ""
    assert(dryos.image_prefix == old_prefix)

1072
1073
1074
1075
1076
    -- edge case if image number is 9994/9995 (next image 9995/9996) before running api_test.lua
    -- images from previous tests will be either (9995, 9996, 9997), or (9996, 9997, 9998)
    -- the next 3 images will be either (100CANON/IMG_9998, 100CANON/IMG_9999, 101CANON/IMG_0001)
    -- or (100CANON/IMG_9999, 101CANON/IMG_0001, 101CANON/IMG_0002)
    -- dryos.shooting_card:image_path takes care of this
1077
1078
1079
1080
1081
1082
1083
1084
    printf("Bracketed pictures...\n")
    initial_file_num = dryos.shooting_card.file_number
    camera.shutter.value = 1/500
    camera.shoot()
    camera.shutter.value = 1/50
    camera.shoot()
    camera.shutter.value = 1/5
    camera.shoot()
1085
    assert((dryos.shooting_card.file_number - initial_file_num) % 9999 == 3)
1086
1087
    camera.wait()
    -- fixme: how to check metadata in the files?
1088
1089
1090
    for i = -2,0 do
        image_path_cr2 = dryos.shooting_card:image_path(i, ".CR2")
        image_path_jpg = dryos.shooting_card:image_path(i, ".JPG")
1091
1092
1093
1094
        local size_cr2 = print_file_size(image_path_cr2)
        local size_jpg = print_file_size(image_path_jpg)
        assert(size_cr2 or size_jpg)
    end
1095

1096
    sleep(2)
1097
1098
1099
1100

    printf("Bulb picture...\n")
    local t0 = dryos.ms_clock
    initial_file_num = dryos.shooting_card.file_number
1101
1102
    image_path_cr2 = dryos.shooting_card:image_path(1, ".CR2")
    image_path_jpg = dryos.shooting_card:image_path(1, ".JPG")
1103
1104