Read about our upcoming Code of Conduct on this issue

api_test.lua 50.7 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
function request_mode(mode, mode_str)
    while camera.mode ~= mode do
20
        printf("Please switch to %s mode.\n", mode_str, mode)
21
        while camera.mode ~= mode do
alex@thinkpad's avatar
alex@thinkpad committed
22
            console.show(); assert(console.visible)
23
            sleep(1)
24
25
26
27
        end
    end
end

a1ex's avatar
a1ex committed
28
29
30
31
32
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

33
34
35
36
37
38
39
40
41
42
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

43
44
45
46
-- used for testing Lua 'strict' mode
declared_var = nil

function strict_tests()
47
    printf("Strict mode tests...\n")
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

    -- 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"))

63
64
    printf("Strict mode tests passed.\n")
    printf("\n")
65
66
67
end

function generic_tests()
68
    printf("Generic tests...\n")
69
    print_table("arg")
70
71
72
73
74
75
76
77
78
79
80
81
82
83
    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")
84
85
    printf("Generic tests completed.\n")
    printf("\n")
86
87
end

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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)
110
    printf("Copy test: %s -> %s\n", src, dst)
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
    
    -- 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
135
    assert(io.open(dst, "rb") == nil)
136
137
138
139

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

140
    printf("Copy test OK\n")
141
142
143
end

function append_test(file)
144
    printf("Append test: %s\n", file)
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

    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
161
    assert(check == data1)
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

    -- 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
178
    assert(io.open(file, "rb") == nil)
179

180
    printf("Append test OK\n")
181
182
end

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
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

221
function test_io()
alex@thinkpad's avatar
alex@thinkpad committed
222
    printf("Testing file I/O...\n")
223
224
225
    stdio_test()
    copy_test("autoexec.bin", "tmp.bin")
    append_test("tmp.txt")
226
227
    rename_test("apple.txt", "banana.txt")
    rename_test("apple.txt", "ML/banana.txt")
alex@thinkpad's avatar
alex@thinkpad committed
228
229
230

    printf("File I/O tests completed.\n")
    printf("\n")
231
232
end

233
234
235
236
237
function test_camera_gui()
    printf("Testing Canon GUI functions...\n")

    camera.gui.play = true
    assert(camera.gui.play == true)
238
    assert(camera.gui.mode == 1)
239
240
241

    -- half-shutter should exit playback mode
    key.press(KEY.HALFSHUTTER)
242
    sleep(1)
243
    assert(camera.gui.play == false)
244
    assert(camera.gui.mode == 0)
245
246
247
248
249
250
251
252
253
254
255
256
257
    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)
258
                assert(camera.gui.mode == 1)
259
260
261
262
263
264
            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)
265
                assert(camera.gui.mode == 0)
266
267
268
269
270
271
272
273
            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)
274
                assert(camera.gui.mode == 2)
275
276
277
278
279
280
            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)
281
                assert(camera.gui.mode == 0)
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
            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()
309
    assert(not lv.enabled)
310
311
312
313
314

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

315
function test_menu()
316
    printf("Testing ML menu API...\n")
317
318
319
320
321
322
323
324

    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)
325
    sleep(1)
326
327
328
329

    -- 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)
330
    sleep(1)
331
332
333
334
335
336
337

    -- 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"))
338
    assert(menu.get("Expo", "Picture Style") == "Portrait")
339
    sleep(1)
340
    assert(menu.set("Expo", "Picture Style", 5)) -- OK, selects Neutral
341
    -- skip string test, as other camera models may use different indices
342
    sleep(1)
343
    assert(menu.set("Expo", "Picture Style", "Landscape"))
344
    assert(menu.get("Expo", "Picture Style") == "Landscape")
345
    sleep(1)
346
    assert(menu.set("Expo", "Picture Style", 1234) == false) -- should fail, out of range
347
    assert(menu.get("Expo", "Picture Style") == "Landscape") -- old selection should not change
348
    sleep(1)
349
350

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

352
353
    assert(menu.select("Movie"))

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
    -- 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"))
388
389
390
391
    assert(menu.select("Shoot", "Advanced Bracket"))

    -- boolean items should be set-able as int (0 or 1)
    assert(menu.set("Shoot", "Advanced Bracket", 1))
392
    assert(menu.get("Shoot", "Advanced Bracket", 0) == 1)
393
    sleep(1)
394
395
396
397

    -- 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"))
398
    assert(menu.get("Shoot", "Intervalometer", 0) == 1)
399
    sleep(1)
400
401
402

    -- turning off should be straightforward
    assert(menu.set("Shoot", "Advanced Bracket", "OFF"))
403
404
    assert(menu.get("Shoot", "Advanced Bracket", 0) == 0)
    assert(menu.get("Shoot", "Advanced Bracket") == "OFF")
405
    sleep(1)
406

407
408
409
410
411
412
413
414
    -- "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")

415
    assert(menu.set("Shoot", "Intervalometer", 0))
416
417
    assert(menu.get("Shoot", "Intervalometer", 0) == 0)
    assert(menu.get("Shoot", "Intervalometer") == "OFF")
418
    sleep(1)
419
420
421

    -- move to Intervalometer menu
    assert(menu.select("Shoot", "Intervalometer"))
422
    sleep(1)
423
424

    -- enter the submenu
425
    assert(menu.select("Intervalometer", "Take a pic every"))
426
    sleep(1)
427
428
429

    -- note: setting menu by string works by brute force
    -- that is, trying every possible value and comparing the string
430
431
432
433
434
    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
435
    assert(menu.set("Intervalometer", "Take a pic every", "1m30s"))
436
437
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 90)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m30s")
438
439
440
441
442

    -- 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")
443
    sleep(1)
444
445
446

    -- actual string will be 10s
    assert(menu.set("Intervalometer", "Take a pic every", "10"))
447
448
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 10)
    assert(menu.get("Intervalometer", "Take a pic every") == "10s")
449
    sleep(1)
450

451
452
    -- 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
453
    assert(menu.set("Intervalometer", "Take a pic every", 70))
454
455
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")
456
    sleep(1)
457
458

    -- out of range, should fail
459
    assert(menu.set("Intervalometer", "Take a pic every", 7000000) == false)
460
461
    assert(menu.get("Intervalometer", "Take a pic every", 0) == 70)
    assert(menu.get("Intervalometer", "Take a pic every") == "1m10s")
462
    sleep(1)
463
464

    -- exit submenu
465
    assert(menu.select("Shoot", "Intervalometer"))
466
    sleep(1)
467

468
469
    -- let's try some more submenus
    -- todo: more multi-level submenus
470
471
472
473
474
475
476
477
478
479
    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)
480

481
482
483
    -- selection in the Modified menu
    -- note: we have already modified "Take a pic every"
    -- so at least this setting should appear there
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
    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
506

507
    -- non-existent menus; should fail
508
509
510
511
    assert(menu.select("Dinosaur") == false)
    assert(menu.select("Shoot", "Crocodile") == false)

    -- menu.get/set return nil if the menu was not found
512
513
    assert(menu.get("Shoot", "Introvolometer", 0) == nil)
    assert(menu.get("Shoot", "Brack") == nil)
514
515
    assert(menu.set("Shoot", "Introvolometer", 1) == nil)
    assert(menu.set("Shoot", "Introvolometer", "OFF") == nil)
516
517
518
519
520
521

    menu.close()

    -- exercise the menu backend a bit
    for i = 1,5 do
        menu.open()
522
        assert(camera.gui.idle == false)
523
        menu.close()
524
        assert(camera.gui.idle == true)
525
526
527
528
529
530
    end

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

531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
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

549
550
551
552
553
554
555
556
557
558
559
560
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

561
562
563
function test_multitasking()
    printf("Testing multitasking...\n")

564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
    -- 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)
588
589
590
591
592

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

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

622
function test_camera_exposure()
alex@thinkpad's avatar
alex@thinkpad committed
623
    printf("Testing exposure settings...\n")
624
625
626
627
628
629
    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",
630
631
632
633
634
        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
    )
635
636
637
    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)
638

639
    request_mode(MODE.M, "M")
640
    local old_value = camera.shutter.raw
641
    printf("Setting shutter to random values...\n")
642
    for k = 1,100 do
643
644
        local method = math.random(1,4)
        local d = nil
645
646
        if method == 1 then
            local s = math.random(1,30)
647
648
649
650
651
            if math.random(1,2) == 1 then
                camera.shutter.value = s
            else
                camera.shutter = s
            end
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
            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
671
            printf("Error: shutter delta %s EV\n", d)
672
673
674
675
        end

        -- seconds and ms fields should be consistent
        if math.abs(camera.shutter.value - camera.shutter.ms/1000) > 1e-3 then
676
            printf("Error: shutter %ss != %sms\n", camera.shutter.value, camera.shutter.ms)
677
678
679
        end
        
        -- seconds and apex should be consistent
680
        -- see http://dougkerr.net/Pumpkin/articles/APEX.pdf
681
        local expected_apex = math.log(1/camera.shutter.value,2)
682
        if math.abs(expected_apex - camera.shutter.apex) > 0.01 then
683
            printf("Error: shutter %ss != Tv%s, expected %s\n", camera.shutter.value, camera.shutter.apex, expected_apex)
684
685
686
687
688
        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
689
690
691
692
693
            local current = {}
            current.apex  = camera.shutter.apex
            current.value = camera.shutter.value
            current.ms    = camera.shutter.ms
            current.raw   = camera.shutter.raw
694
695
696
            
            -- 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
697
            if field == "ms" and current.ms < 10 and method ~= 2 then
698
699
700
701
                -- how do you write "continue" in Lua?!
                field = "value"
            end
            
702
            camera.shutter[field] = current[field]
703
            
704
            if camera.shutter.value ~= current.value then
705
                printf("Error: shutter set to %s=%s, got %ss, expected %ss\n", field, current[field], camera.shutter.value, current.value)
706
            end
707
            if camera.shutter.ms ~= current.ms then
708
                printf("Error: shutter set to %s=%s, got %sms, expected %sms\n", field, current[field], camera.shutter.ms, current.ms)
709
            end
710
            if camera.shutter.apex ~= current.apex then
711
                printf("Error: shutter set to %s=%s, got Tv%s, expected Tv%s\n", field, current[field], camera.shutter.apex, current.apex)
712
            end
713
            if camera.shutter.raw ~= current.raw then
714
                printf("Error: shutter set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.shutter.raw, current.raw)
715
716
717
            end
        end
    end
718
    camera.shutter.raw = old_value
719

720
    request_mode(MODE.M, "M")
721
    old_value = camera.iso.raw
722
    printf("Setting ISO to random values...\n")
723
    for k = 1,100 do
724
725
        local method = math.random(1,3)
        local d = nil
726
727
        if method == 1 then
            local iso = math.random(100, 6400)
728
729
730
731
732
            if math.random(1,2) == 1 then
                camera.iso.value = iso
            else
                camera.iso = iso
            end
733
734
735
736
737
738
739
740
741
742
743
744
745
            d = math.abs(math.log(camera.iso.value,2) - math.log(iso,2))
        elseif method == 2 then
            local apex = math.random(5*100,11*100)/100
            camera.iso.apex = apex
            d = math.abs(camera.iso.apex - apex)
        elseif method == 3 then
            local raw = math.random(72, 120)
            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
746
            printf("Error: ISO delta %s EV\n", d)
747
748
749
        end

        -- ISO and Sv (APEX) should be consistent
750
        local expected_apex = math.log(camera.iso.value/3.125, 2)
751
        if math.abs(expected_apex - camera.iso.apex) > 0.2 then
752
            printf("Error: ISO %s != Sv%s, expected %s\n", camera.iso.value, camera.iso.apex, expected_apex)
753
754
755
756
757
        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
758
759
760
761
            local current = {}
            current.apex  = camera.iso.apex
            current.value = camera.iso.value
            current.raw   = camera.iso.raw
762
            
763
            camera.iso[field] = current[field]
764
            
765
            if camera.iso.value ~= current.value then
766
                printf("Error: ISO set to %s=%s, got %s, expected %s\n", field, current[field], camera.iso.value, current.value)
767
            end
768
            if camera.iso.apex ~= current.apex then
769
                printf("Error: ISO set to %s=%s, got Sv%s, expected Sv%s\n", field, current[field], camera.iso.apex, current.apex)
770
            end
771
            if camera.iso.raw ~= current.raw then
772
                printf("Error: ISO set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.iso.raw, current.raw)
773
774
775
            end
        end
    end
776
    camera.iso.raw = old_value
777

a1ex's avatar
a1ex committed
778
    if camera.aperture.min.raw == camera.aperture.max.raw then
779
        printf("This lens does not have variable aperture (skipping test).\n")
a1ex's avatar
a1ex committed
780
781
    else
        request_mode(MODE.M, "M")
782
        old_value = camera.aperture.raw
783
        printf("Setting aperture to random values...\n")
a1ex's avatar
a1ex committed
784
        for k = 1,100 do
785
786
787
            local method = math.random(1,3)
            local d = nil
            local extra_tol = 0
a1ex's avatar
a1ex committed
788
789
            if method == 1 then
                local av = math.random(round(camera.aperture.min.value*10), round(camera.aperture.max.value*10)) / 10
790
791
792
793
794
                if math.random(1,2) == 1 then
                    camera.aperture.value = av
                else
                    camera.aperture = av
                end
a1ex's avatar
a1ex committed
795
                d = math.abs(math.log(camera.aperture.value,2) - math.log(av,2)) * 2
796
797
                -- 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
798
799
800
801
802
803
804
805
806
807
808
            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
809
810
811
            -- 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
812
                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
813
814
815
            end

            -- aperture and Av (APEX) should be consistent
816
            local expected_apex = math.log(camera.aperture.value, 2) * 2
a1ex's avatar
a1ex committed
817
            if math.abs(expected_apex - camera.aperture.apex) > 0.2 then
818
                printf("Error: aperture %s != Av%s, expected %s\n", camera.aperture.value, camera.aperture.apex, expected_apex)
a1ex's avatar
a1ex committed
819
820
821
822
823
            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
824
825
826
827
                local current = {}
                current.apex  = camera.aperture.apex
                current.value = camera.aperture.value
                current.raw   = camera.aperture.raw
a1ex's avatar
a1ex committed
828
                
829
                camera.aperture[field] = current[field]
a1ex's avatar
a1ex committed
830
                
831
                if camera.aperture.value ~= current.value then
832
                    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
833
                end
834
                if camera.aperture.apex ~= current.apex then
835
                    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
836
                end
837
                if camera.aperture.raw ~= current.raw then
838
                    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
839
840
841
                end
            end
        end
842
        camera.aperture.raw = old_value
a1ex's avatar
a1ex committed
843
844
    end

845
    request_mode(MODE.AV, "Av")
846
    old_value = camera.ec.raw
847
    printf("Setting EC to random values...\n")
848
    for k = 1,100 do
849
850
        local method = math.random(1,2)
        local d = nil
851
852
        if method == 1 then
            local ec = math.random(-2*100, 2*100) / 100
853
854
855
856
857
            if math.random(1,2) == 1 then
                camera.ec.value = ec
            else
                camera.ec = ec
            end
858
859
860
861
862
863
864
865
866
            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
867
            printf("Error: EC delta %s EV\n", d)
868
869
870
        end

        -- EC and raw should be consistent
871
        local expected_ec = camera.ec.raw / 8
872
        if math.abs(expected_ec - camera.ec.value) > 0.001 then
873
            printf("Error: EC raw %s != %s EV, expected %s EV\n", camera.ec.raw, camera.ec.value, expected_ec)
874
875
876
877
878
        end

        -- setting EC to the same value, using any method (value,raw)
        -- should not change anything
        for i,field in pairs{"value","raw"} do
879
880
881
            local current = {}
            current.value = camera.ec.value
            current.raw   = camera.ec.raw
882
            
883
            camera.ec[field] = current[field]
884
            
885
            if camera.ec.value ~= current.value then
886
                printf("Error: EC set to %s=%s, got %s, expected %s EV\n", field, current[field], camera.ec.value, current.value)
887
            end
888
            if camera.ec.raw ~= current.raw then
889
                printf("Error: EC set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.ec.raw, current.raw)
890
891
892
            end
        end
    end
893
    camera.ec.raw = old_value
894
895

    -- copy/paste & replace from EC (those two should behave in the same way)
896
    old_value = camera.flash_ec.raw
897
    printf("Setting Flash EC to random values...\n")
898
    for k = 1,100 do
899
900
        local method = math.random(1,2)
        local d = nil
901
902
        if method == 1 then
            local fec = math.random(-2*100, 2*100) / 100
903
904
905
906
907
            if math.random(1,2) == 1 then
                camera.flash_ec.value = fec
            else
                camera.flash_ec = fec
            end
908
909
910
911
912
913
914
915
916
            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
917
            printf("Error: FEC delta %s EV\n", d)
918
919
920
        end

        -- EC and raw should be consistent
921
        local expected_fec = camera.flash_ec.raw / 8
922
        if math.abs(expected_fec - camera.flash_ec.value) > 0.001 then
923
            printf("Error: FEC raw %s != %s EV, expected %s EV\n", camera.flash_ec.raw, camera.flash_ec.value, current.expected_fec)
924
925
926
927
928
        end

        -- setting EC to the same value, using any method (value,raw)
        -- should not change anything
        for i,field in pairs{"value","raw"} do
929
930
931
            local current = {}
            current.value = camera.flash_ec.value
            current.raw   = camera.flash_ec.raw
932
            
933
            camera.flash_ec[field] = current[field]
934
            
935
            if camera.flash_ec.value ~= current.value then
936
                printf("Error: FEC set to %s=%s, got %s, expected %s EV\n", field, current[field], camera.flash_ec.value, current.value)
937
            end
938
            if camera.flash_ec.raw ~= current.raw then
939
                printf("Error: FEC set to %s=%s, got %s, expected %s (raw)\n", field, current[field], camera.flash_ec.raw, current.raw)
940
941
942
            end
        end
    end
943
    camera.flash_ec.raw = old_value
944

945
946
    printf("Exposure tests completed.\n")
    printf("\n")
947
948
end

949
950
951
952
953
954
955
956
957
958
959
960
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

961
function test_camera_take_pics()
alex@thinkpad's avatar
alex@thinkpad committed
962
    printf("Testing picture taking functions...\n")
963
964
965
966
    local initial_file_num

    request_mode(MODE.M, "M")
    camera.shutter = 1/50
967
    sleep(2)
968
969
970
971
972
973
974
975
    
    printf("Snap simulation test...\n")
    assert(menu.set("Shoot Preferences", "Snap Simulation", 1))
    initial_file_num = dryos.shooting_card.file_number
    camera.shoot()
    assert(dryos.shooting_card.file_number == initial_file_num)
    assert(menu.set("Shoot Preferences", "Snap Simulation", 0))

976
    sleep(2)
977
978

    printf("Single picture...\n")
979
980
    -- let's also check if we can find the image file
    -- fixme: a way to check these routines when the image number wraps around at 10000
981
    initial_file_num = dryos.shooting_card.file_number
982
983
984
985
986
987
988
    local image_path = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", (initial_file_num + 1) % 10000)
    local image_path_cr2 = image_path .. ".CR2"
    local image_path_jpg = image_path .. ".JPG"
    -- 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)

989
    camera.shoot()
990
991

    -- but either CR2 or JPG should be there afterwards (or maybe both)
992
    assert((dryos.shooting_card.file_number - initial_file_num) % 10000 == 1)
993
994
995
996
997
    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)

998
999
1000
1001
1002
1003
1004
    -- 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)

1005
    sleep(2)
1006
1007
1008
1009

    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
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025

    -- 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_")
    local image1_path = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", (initial_file_num + 1) % 10000)
    local image1_path_cr2 = image1_path .. ".CR2"
    local image1_path_jpg = image1_path .. ".JPG"
    local image2_path = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", (initial_file_num + 2) % 10000)
    local image2_path_cr2 = image2_path .. ".CR2"
    local image2_path_jpg = image2_path .. ".JPG"
    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)

1026
    camera.burst(2)
1027

1028
    assert((dryos.shooting_card.file_number - initial_file_num) % 10000 == 2)
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
    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)

    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()
    assert((dryos.shooting_card.file_number - initial_file_num) % 10000 == 3)
    camera.wait()
    -- fixme: how to check metadata in the files?
    for i = dryos.shooting_card.file_number - 2, dryos.shooting_card.file_number do
        image_path = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", i % 10000)
        image_path_cr2 = image_path .. ".CR2"
        image_path_jpg = image_path .. ".JPG"
        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
1068

1069
    sleep(2)
1070
1071
1072
1073

    printf("Bulb picture...\n")
    local t0 = dryos.ms_clock
    initial_file_num = dryos.shooting_card.file_number
1074
1075
1076
1077
1078
1079
    image_path = dryos.dcim_dir.path ..  dryos.image_prefix .. string.format("%04d", (initial_file_num + 1) % 10000)
    image_path_cr2 = image_path .. ".CR2"
    image_path_jpg = image_path .. ".JPG"
    assert(io.open(image_path_cr2, "rb") == nil)
    assert(io.open(image_path_jpg, "rb") == nil)

1080
    camera.bulb(10)
1081

1082
1083
1084
1085
    local t1 = dryos.ms_clock
    local elapsed = t1 - t0
    printf("Elapsed time: %s\n", elapsed)
    -- we can't measure this time accurately, so we only do a very rough check
1086
1087
    -- slow cards may be an issue, so let's allow a wide error margin
    assert(elapsed > 9900 and elapsed < 30000)
1088
    assert((dryos.shooting_card.file_number - initial_file_num) % 10000 == 1)
1089
1090
1091
1092
    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)
1093
1094

    printf("Picture taking tests completed.\n")
alex@thinkpad's avatar
alex@thinkpad committed
1095
    printf("\n")
1096
    sleep(5)
1097
1098
end

1099
function test_lv()
1100
1101
    request_mode(MODE.M, "M")

1102
    printf("Testing module 'lv'...\n")
1103
    if lv.enabled then
1104
        printf("LiveView is running; stopping...\n")
1105
1106
        lv.stop()
        assert(not lv.enabled, "LiveView did not stop")
1107
        sleep(2)
1108
1109
    end

1110
1111
1112
    assert(not lv.enabled)
    assert(lv.vidmode == "PH-NOLV")

1113
    printf("Starting LiveView...\n")
1114
1115
    lv.start()
    assert(lv.enabled, "LiveView did not start")
1116
    assert(not lens.autofocusing)