From e53586a1130984f1252a4d7c5bdefaf355a1d8f9 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Thu, 27 Mar 2014 00:38:03 +0000 Subject: [PATCH 1/7] Tests should create virtual device instead of fail. sane-test(5) provides a virtual device backend, designed for testing software that interacts with SANE. If PyInsane tests fail to find any devices, we should create a test device instead. Signed-off-by: Ross Vandegrift --- run_tests.py | 1 - tests/tests_abstract.py | 5 +++++ tests/tests_rawapi.py | 21 ++++++++++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/run_tests.py b/run_tests.py index a84f3bc..bf427e7 100755 --- a/run_tests.py +++ b/run_tests.py @@ -10,7 +10,6 @@ from src import abstract from src import abstract_th if __name__ == '__main__': - print("Don't forget to turn at least one scanner on !") #print("---") #print("=== RawAPI: ===") #unittest.TextTestRunner(verbosity=3).run( diff --git a/tests/tests_abstract.py b/tests/tests_abstract.py index 51fa6ff..7d3890c 100644 --- a/tests/tests_abstract.py +++ b/tests/tests_abstract.py @@ -15,6 +15,11 @@ class TestSaneGetDevices(unittest.TestCase): def test_get_devices(self): devices = self.module.get_devices() + if len(devices) == 0: + # if there are no devices found, create a virtual device. + # see sane-test(5) and /etc/sane.d/test.conf + self.module.Scanner("test")._open() + devices = self.module.get_devices() self.assertTrue(len(devices) > 0) def tearDown(self): diff --git a/tests/tests_rawapi.py b/tests/tests_rawapi.py index 207fc92..69fc6d2 100644 --- a/tests/tests_rawapi.py +++ b/tests/tests_rawapi.py @@ -6,6 +6,17 @@ import unittest import rawapi +def get_test_devices(): + '''Return SANE devices, perhaps after creating a test device.''' + devices = rawapi.sane_get_devices() + if len(devices) == 0: + # if there are no devices found, create a virtual device. + # see sane-test(5) and /etc/sane.d/test.conf + rawapi.sane_close(rawapi.sane_open("test")) + devices = rawapi.sane_get_devices() + return devices + + class TestSaneInit(unittest.TestCase): def setUp(self): pass @@ -23,7 +34,7 @@ class TestSaneGetDevices(unittest.TestCase): rawapi.sane_init() def test_get_devices(self): - devices = rawapi.sane_get_devices() + devices = get_test_devices() self.assertTrue(len(devices) > 0) def tearDown(self): @@ -33,7 +44,7 @@ class TestSaneGetDevices(unittest.TestCase): class TestSaneOpen(unittest.TestCase): def setUp(self): rawapi.sane_init() - devices = rawapi.sane_get_devices() + devices = get_test_devices() self.assertTrue(len(devices) > 0) self.dev_name = devices[0].name @@ -51,7 +62,7 @@ class TestSaneOpen(unittest.TestCase): class TestSaneGetOptionDescriptor(unittest.TestCase): def setUp(self): rawapi.sane_init() - devices = rawapi.sane_get_devices() + devices = get_test_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) @@ -85,7 +96,7 @@ class TestSaneGetOptionDescriptor(unittest.TestCase): class TestSaneControlOption(unittest.TestCase): def setUp(self): rawapi.sane_init() - devices = rawapi.sane_get_devices() + devices = get_test_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) @@ -122,7 +133,7 @@ class TestSaneControlOption(unittest.TestCase): class TestSaneScan(unittest.TestCase): def setUp(self): rawapi.sane_init() - devices = rawapi.sane_get_devices() + devices = get_test_devices() self.assertTrue(len(devices) > 0) dev_name = devices[0].name self.dev_handle = rawapi.sane_open(dev_name) -- GitLab From c73e75309f6d8623e19f33f52d0a3c1b8ade01a0 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Thu, 27 Mar 2014 00:42:12 +0000 Subject: [PATCH 2/7] If a scanner can't perform a function, skip the test. Some of the tests assume specific things about the capabilities of the devices currently active. Running tests against sane-test and an fi-4120C2 revealed these don't always hold. Signed-off-by: Ross Vandegrift --- tests/tests_abstract.py | 59 +++++++++++++++++++++++++++-------------- tests/tests_rawapi.py | 10 +++++-- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/tests/tests_abstract.py b/tests/tests_abstract.py index 7d3890c..580cc19 100644 --- a/tests/tests_abstract.py +++ b/tests/tests_abstract.py @@ -60,7 +60,6 @@ class TestSaneOptions(unittest.TestCase): del(dev) del(self.devices) - class TestSaneScan(unittest.TestCase): def set_module(self, module): self.module = module @@ -71,8 +70,10 @@ class TestSaneScan(unittest.TestCase): self.dev = devices[0] def test_simple_scan_lineart(self): - self.assertTrue("Lineart" in self.dev.options['mode'].constraint) - self.dev.options['mode'].value = "Lineart" + try: + self.dev.options['mode'].value = "Lineart" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) try: assert(scan_session.scan is not None) @@ -84,8 +85,10 @@ class TestSaneScan(unittest.TestCase): self.assertNotEqual(img, None) def test_simple_scan_gray(self): - self.assertTrue("Gray" in self.dev.options['mode'].constraint) - self.dev.options['mode'].value = "Gray" + try: + self.dev.options['mode'].value = "Gray" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) try: while True: @@ -96,8 +99,10 @@ class TestSaneScan(unittest.TestCase): self.assertNotEqual(img, None) def test_simple_scan_color(self): - self.assertTrue("Color" in self.dev.options['mode'].constraint) - self.dev.options['mode'].value = "Color" + try: + self.dev.options['mode'].value = "Color" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) try: while True: @@ -108,9 +113,11 @@ class TestSaneScan(unittest.TestCase): self.assertNotEqual(img, None) def test_multi_scan_on_flatbed(self): - self.assertTrue("Flatbed" in self.dev.options['source'].constraint) - self.dev.options['source'].value = "Flatbed" - self.dev.options['mode'].value = "Color" + try: + self.dev.options['source'].value = "Flatbed" + self.dev.options['mode'].value = "Color" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=True) try: while True: @@ -121,9 +128,17 @@ class TestSaneScan(unittest.TestCase): self.assertNotEqual(scan_session.images[0], None) def test_multi_scan_on_adf(self): - self.assertTrue("ADF" in self.dev.options['source'].constraint) - self.dev.options['source'].value = "ADF" - self.dev.options['mode'].value = "Color" + # sane-test uses 'Automatic Document Feeder' instead of ADF + try: + if "ADF" in self.dev.options['source'].constraint: + self.dev.options['source'].value = "ADF" + pages = 0 + elif "Automatic Document Feeder" in self.dev.options['source'].constraint: + self.dev.options['source'].value = "Automatic Document Feeder" + pages = 10 # sane-test scans give us 10 pages + self.dev.options['mode'].value = "Color" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=True) try: while True: @@ -133,12 +148,14 @@ class TestSaneScan(unittest.TestCase): pass except StopIteration: pass - self.assertEqual(len(scan_session.images), 0) + self.assertEqual(len(scan_session.images), pages) def test_expected_size(self): - self.assertTrue("ADF" in self.dev.options['source'].constraint) - self.dev.options['source'].value = "Flatbed" - self.dev.options['mode'].value = "Color" + try: + self.dev.options['source'].value = "Flatbed" + self.dev.options['mode'].value = "Color" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) scan_size = scan_session.scan.expected_size self.assertTrue(scan_size[0] > 100) @@ -146,9 +163,11 @@ class TestSaneScan(unittest.TestCase): scan_session.scan.cancel() def test_get_progressive_scan(self): - self.assertTrue("ADF" in self.dev.options['source'].constraint) - self.dev.options['source'].value = "Flatbed" - self.dev.options['mode'].value = "Color" + try: + self.dev.options['source'].value = "Flatbed" + self.dev.options['mode'].value = "Color" + except self.module.SaneException: + self.skipTest("scanner does not support required option") scan_session = self.dev.scan(multiple=False) last_line = 0 expected_size = scan_session.scan.expected_size diff --git a/tests/tests_rawapi.py b/tests/tests_rawapi.py index 69fc6d2..4fef1e2 100644 --- a/tests/tests_rawapi.py +++ b/tests/tests_rawapi.py @@ -143,7 +143,10 @@ class TestSaneScan(unittest.TestCase): # with my scanner #rawapi.sane_set_io_mode(self.dev_handle, non_blocking=False) - rawapi.sane_start(self.dev_handle) + try: + rawapi.sane_start(self.dev_handle) + except StopIteration: + self.skipTest("cannot scan, no document loaded") # XXX(Jflesch): get_select_fd() always return SANE_STATUS_UNSUPPORTED # with my scanner @@ -159,7 +162,10 @@ class TestSaneScan(unittest.TestCase): rawapi.sane_cancel(self.dev_handle) def test_cancelled_scan(self): - rawapi.sane_start(self.dev_handle) + try: + rawapi.sane_start(self.dev_handle) + except StopIteration: + self.skipTest("cannot scan, no document loaded") buf = rawapi.sane_read(self.dev_handle, 128*1024) self.assertTrue(len(buf) > 0) rawapi.sane_cancel(self.dev_handle) -- GitLab From 18863cab286ac159268d4292b5b7c3c82d6a4f26 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Sat, 29 Mar 2014 19:35:45 +0000 Subject: [PATCH 3/7] Fix rawapi tests and enable them The rawapi tests run into strange SANE behavior: some SANE options cannot be read, set, or forced to auto. For now, mark the test as an expected failure. Signed-off-by: Ross Vandegrift --- run_tests.py | 8 ++++---- tests/tests_rawapi.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/run_tests.py b/run_tests.py index bf427e7..69c91aa 100755 --- a/run_tests.py +++ b/run_tests.py @@ -10,10 +10,10 @@ from src import abstract from src import abstract_th if __name__ == '__main__': - #print("---") - #print("=== RawAPI: ===") - #unittest.TextTestRunner(verbosity=3).run( - # tests_rawapi.get_all_tests()) + print("---") + print("=== RawAPI: ===") + unittest.TextTestRunner(verbosity=3).run( + tests_rawapi.get_all_tests()) print("---") print("=== Abstract: ===") unittest.TextTestRunner(verbosity=3).run( diff --git a/tests/tests_rawapi.py b/tests/tests_rawapi.py index 4fef1e2..483c134 100644 --- a/tests/tests_rawapi.py +++ b/tests/tests_rawapi.py @@ -102,6 +102,9 @@ class TestSaneControlOption(unittest.TestCase): self.dev_handle = rawapi.sane_open(dev_name) self.nb_options = rawapi.sane_get_option_value(self.dev_handle, 0) + # This test fails because libsane gives us back some options that + # we can't do anything with --- including get their value + @unittest.expectedFailure def test_get_option_value(self): for opt_idx in range(0, self.nb_options): desc = rawapi.sane_get_option_descriptor(self.dev_handle, opt_idx) -- GitLab From 4f1c85ee6753906b17a00d774c20eaf5768b5831 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Sat, 29 Mar 2014 20:11:06 +0000 Subject: [PATCH 4/7] Revert "Fix rawapi tests and enable them" This reverts commit 18863cab286ac159268d4292b5b7c3c82d6a4f26. --- run_tests.py | 8 ++++---- tests/tests_rawapi.py | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/run_tests.py b/run_tests.py index 69c91aa..bf427e7 100755 --- a/run_tests.py +++ b/run_tests.py @@ -10,10 +10,10 @@ from src import abstract from src import abstract_th if __name__ == '__main__': - print("---") - print("=== RawAPI: ===") - unittest.TextTestRunner(verbosity=3).run( - tests_rawapi.get_all_tests()) + #print("---") + #print("=== RawAPI: ===") + #unittest.TextTestRunner(verbosity=3).run( + # tests_rawapi.get_all_tests()) print("---") print("=== Abstract: ===") unittest.TextTestRunner(verbosity=3).run( diff --git a/tests/tests_rawapi.py b/tests/tests_rawapi.py index 483c134..4fef1e2 100644 --- a/tests/tests_rawapi.py +++ b/tests/tests_rawapi.py @@ -102,9 +102,6 @@ class TestSaneControlOption(unittest.TestCase): self.dev_handle = rawapi.sane_open(dev_name) self.nb_options = rawapi.sane_get_option_value(self.dev_handle, 0) - # This test fails because libsane gives us back some options that - # we can't do anything with --- including get their value - @unittest.expectedFailure def test_get_option_value(self): for opt_idx in range(0, self.nb_options): desc = rawapi.sane_get_option_descriptor(self.dev_handle, opt_idx) -- GitLab From 5af69d023d7c6ff73b5ed3e2c1c68122096d04f5 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Sat, 29 Mar 2014 20:12:18 +0000 Subject: [PATCH 5/7] Fix rawapi tests and enable them When testing option getting, we need to check the option capabilities for their active status. Inactive options cannot be read. Signed-off-by: Ross Vandegrift --- run_tests.py | 8 ++++---- tests/tests_rawapi.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/run_tests.py b/run_tests.py index bf427e7..04a525c 100755 --- a/run_tests.py +++ b/run_tests.py @@ -10,10 +10,10 @@ from src import abstract from src import abstract_th if __name__ == '__main__': - #print("---") - #print("=== RawAPI: ===") - #unittest.TextTestRunner(verbosity=3).run( - # tests_rawapi.get_all_tests()) + print("---") + print("=== RawAPI: ===") + unittest.TextTestRunner(verbosity=3).run( + tests_rawapi.get_all_tests()) print("---") print("=== Abstract: ===") unittest.TextTestRunner(verbosity=3).run( diff --git a/tests/tests_rawapi.py b/tests/tests_rawapi.py index 4fef1e2..3723548 100644 --- a/tests/tests_rawapi.py +++ b/tests/tests_rawapi.py @@ -107,6 +107,8 @@ class TestSaneControlOption(unittest.TestCase): desc = rawapi.sane_get_option_descriptor(self.dev_handle, opt_idx) if not rawapi.SaneValueType(desc.type).can_getset_opt(): continue + if desc.cap|rawapi.SaneCapabilities.INACTIVE == desc.cap: + continue val = rawapi.sane_get_option_value(self.dev_handle, opt_idx) self.assertNotEqual(val, None) -- GitLab From dd3612a3786d8cf6432d2a55caf6279b9f3dc549 Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Sat, 29 Mar 2014 21:05:27 +0000 Subject: [PATCH 6/7] Test behavior of getting/setting inactive options. When testing option getting, get every option and ensure that any inactive ones raise an exception. Add a test for setting an inactive option. Signed-off-by: Ross Vandegrift --- tests/tests_abstract.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/tests_abstract.py b/tests/tests_abstract.py index 580cc19..f2e2b1a 100644 --- a/tests/tests_abstract.py +++ b/tests/tests_abstract.py @@ -36,8 +36,11 @@ class TestSaneOptions(unittest.TestCase): def test_get_option(self): for dev in self.devices: - val = dev.options['mode'].value - self.assertNotEqual(val, None) + for (k, v) in dev.options.items(): + if v.capabilities.is_active(): + self.assertNotEqual(v.value, None) + else: + self.assertRaises(self.module.SaneException, lambda: v.value) def test_set_option(self): for dev in self.devices: @@ -45,15 +48,25 @@ class TestSaneOptions(unittest.TestCase): val = dev.options['mode'].value self.assertEqual(val, "Gray") - def __set_opt(self, opt_name, opt_val): - for dev in self.devices: - dev.options[opt_name].value = opt_val + def __set_opt(self, dev, opt_name, opt_val): + dev.options[opt_name].value = opt_val def test_set_inexisting_option(self): - self.assertRaises(KeyError, self.__set_opt, 'xyz', "Gray") + for dev in self.devices: + self.assertRaises(KeyError, self.__set_opt, dev, 'xyz', "Gray") def test_set_invalid_value(self): - self.assertRaises(self.module.SaneException, self.__set_opt, 'mode', "XYZ") + for dev in self.devices: + self.assertRaises(self.module.SaneException, self.__set_opt, dev, 'mode', "XYZ") + + def test_set_inactive_option(self): + for dev in self.devices: + noncolor = [x for x in dev.options["mode"].constraint if x != "Color"] + if len(noncolor) == 0: + self.skipTest("scanner does not support required option") + dev.options["mode"].value = noncolor[0] + # three-pass mode is only active in color mode + self.assertRaises(self.module.SaneException, self.__set_opt, dev, 'three-pass', 1) def tearDown(self): for dev in self.devices: @@ -214,6 +227,7 @@ def get_all_tests(module): TestSaneOptions("test_set_option"), TestSaneOptions("test_set_inexisting_option"), TestSaneOptions("test_set_invalid_value"), + TestSaneOptions("test_set_inactive_option"), ] for test in tests: test.set_module(module) -- GitLab From 656a49665e6a69170d35afd17c77b394b0a43f1a Mon Sep 17 00:00:00 2001 From: Ross Vandegrift Date: Tue, 1 Apr 2014 09:11:32 -0400 Subject: [PATCH 7/7] Move sane_exit workaround to rawapi Calling sane_exit is crashy - it often triggers segfaults on process exit and prevents the rawapi tests from being run reliably. This moves the abstract workaround to the rawapi so we should never sane_exit. Signed-off-by: Ross Vandegrift --- src/abstract.py | 25 +------------------------ src/rawapi.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/abstract.py b/src/abstract.py index df12ecf..d3514c7 100644 --- a/src/abstract.py +++ b/src/abstract.py @@ -13,6 +13,7 @@ from .rawapi import SaneException from .rawapi import SaneStatus from .rawapi import SaneUnit from .rawapi import SaneValueType +from .rawapi import sane_init, sane_exit __all__ = [ @@ -32,36 +33,12 @@ __all__ = [ # We use huge buffers to spend the maximum amount of time in non-Python code SANE_READ_BUFSIZE = 512*1024 -sane_is_init = 0 -sane_version = None - # XXX(Jflesch): Never open more than one handle at the same time. # Some Sane backends don't support it. For instance, I have 2 HP scanners, and # if I try to access both from the same process, I get I/O errors. sane_dev_handle = ("", None) -def sane_init(): - global sane_is_init - global sane_version - if sane_is_init <= 0: - sane_version = rawapi.sane_init() - sane_is_init += 1 - return sane_version - - -def sane_exit(): - # TODO(Jflesch): This is a workaround - # In a multithreaded environment, for some unknown reason, - # calling sane_exit() will work but the program will crash - # when stopping. So we simply never call sane_exit() ... - pass - #global sane_is_init - #sane_is_init -= 1 - #if sane_is_init <= 0: - # rawapi.sane_exit() - - class ScannerOption(object): idx = 0 name = "" diff --git a/src/rawapi.py b/src/rawapi.py index 104d695..f96ecda 100644 --- a/src/rawapi.py +++ b/src/rawapi.py @@ -36,6 +36,8 @@ __all__ = [ 'sane_strstatus', ] +sane_is_init = 0 +sane_version = None try: SANE_LIB = ctypes.cdll.LoadLibrary("libsane.so.1") @@ -497,9 +499,13 @@ def is_sane_available(): def sane_init(auth_callback=__dummy_auth_callback): - global sane_available + global sane_available, sane_is_init, sane_version assert(sane_available) + if sane_is_init > 0: + return sane_version + + sane_is_init += 1 version_code = ctypes.c_int() wrap_func = __AuthCallbackWrapper(auth_callback).wrapper auth_callback = AUTH_CALLBACK_DEF(wrap_func) @@ -511,14 +517,22 @@ def sane_init(auth_callback=__dummy_auth_callback): major = (version_code >> 24) & 0xFF minor = (version_code >> 16) & 0xFF build = (version_code >> 0) & 0xFFFF - return SaneVersion(major, minor, build) + sane_version = SaneVersion(major, minor, build) + return sane_version def sane_exit(): - global sane_available + global sane_available, sane_is_init assert(sane_available) - SANE_LIB.sane_exit() + # TODO(Jflesch): This is a workaround + # In a multithreaded environment, for some unknown reason, + # calling sane_exit() will work but the program will crash + # when stopping. So we simply never call sane_exit() ... + + # sane_is_init -= 1 + # if sane_is_init <= 0: + # SANE_LIB.sane_exit() def sane_get_devices(local_only=False): -- GitLab