Repository navigation
Initial unit testing framework and tests #7026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
7e21223
f738b7d
baad666
266c751
a8f946e
bddfab6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,3 +39,4 @@ uninstaller/UAC.nsh | |
| *.pyc | ||
| *.pyo | ||
| *.dmp | ||
| tests/unit/nvda.ini | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,9 +56,11 @@ install: | |
|
|
||
| build_script: | ||
| - ps: | | ||
| $sconsArgs = "launcher version=$env:version" | ||
| $sconsOutTargets = "launcher" | ||
| $sconsArgs = "version=$env:version" | ||
| if ($env:release) { | ||
| $sconsArgs += " changes userGuide developerGuide release=1" | ||
| $sconsOutTargets += " changes userGuide developerGuide" | ||
| $sconsArgs += " release=1" | ||
| } | ||
| if ($env:versionType) { | ||
| $sconsArgs += " updateVersionType=$env:versionType" | ||
|
|
@@ -68,9 +70,14 @@ build_script: | |
| $sconsArgs += " version_build=$env:APPVEYOR_BUILD_NUMBER" | ||
| # We use cmd to run scons because PowerShell throws exceptions if warnings get dumped to stderr. | ||
| # It's possible to work around this, but the workarounds have annoying side effects. | ||
| Set-AppveyorBuildVariable "sconsOutTargets" $sconsOutTargets | ||
| Set-AppveyorBuildVariable "sconsArgs" $sconsArgs | ||
| - 'echo scons args: %sconsArgs%' | ||
| - py scons.py %sconsArgs% | ||
| - py scons.py source %sconsArgs% | ||
| # We don't need launcher to run tests, so run the tests before launcher. | ||
| - py scons.py tests %sconsArgs% | ||
| - 'echo scons output targets: %sconsOutTargets%' | ||
| - py scons.py %sconsOutTargets% %sconsArgs% | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each of these lines ending with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know why GitHub is flagging this. All of these files have Windows line endings all the way through (as is generally our convention)... and looking at the raw content (outside of an editor), there's nothing special about these lines. |
||
| # Build symbol store. | ||
| - ps: | | ||
| foreach ($syms in | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| ### | ||
| #This file is a part of the NVDA project. | ||
| #URL: http://www.nvda-project.org/ | ||
| #Copyright 2006-2010 NVDA contributers. | ||
| #Copyright 2014-2017 NV Access Limited. | ||
| #This program is free software: you can redistribute it and/or modify | ||
| #it under the terms of the GNU General Public License version 2.0, as published by | ||
| #the Free Software Foundation. | ||
|
|
@@ -24,5 +24,10 @@ tlbFile,headerFile,iidSourceFile,proxySourceFile,dlldataSourceFile=env.TypeLibra | |
| source=idlFile, | ||
| MIDLFLAGS=['/c_ext','/I',Dir('.')], | ||
| ) | ||
| # hack: Ignore midl.exe when deciding to rebuild, as its position in the dependencies | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to open an issue somewhere to keep track of this? It might be helpful for someone who wants to investigate, and see what has been tried. Having the issue number in the comments really helps people track down where the information is. |
||
| # is different in the run before the idl files are copied versus subsequent runs. | ||
| midl=env.WhereIs(env["MIDL"]) | ||
| for target in (tlbFile,headerFile,iidSourceFile,proxySourceFile,dlldataSourceFile): | ||
| env.Ignore(target,midl) | ||
|
|
||
| Return(['tlbFile','headerFile','iidSourceFile','proxySourceFile','dlldataSourceFile']) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| #nvwave.py | ||
| #A part of NonVisual Desktop Access (NVDA) | ||
| #Copyright (C) 2006-2008 NVDA Contributors <http://www.nvda-project.org/> | ||
| #Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy | ||
| #This file is covered by the GNU General Public License. | ||
| #See the file COPYING for more details. | ||
|
|
||
|
|
@@ -11,6 +11,7 @@ | |
| from ctypes import * | ||
| from ctypes.wintypes import * | ||
| import time | ||
| import atexit | ||
| import wx | ||
| import winKernel | ||
| import wave | ||
|
|
@@ -345,3 +346,11 @@ def playWaveFile(fileName, async=True): | |
| fileWavePlayerThread.start() | ||
| else: | ||
| fileWavePlayer.idle() | ||
|
|
||
| # When exiting, ensure fileWavePlayer is deleted before modules get cleaned up. | ||
| # Otherwise, WavePlayer.__del__ will fail with an exception. | ||
| @atexit.register | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a windows line ending here too. |
||
| def _cleanup(): | ||
| global fileWavePlayer, fileWavePlayerThread | ||
| fileWavePlayer = None | ||
| fileWavePlayerThread = None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,12 @@ | ||
| #tones.py | ||
| #A part of NonVisual Desktop Access (NVDA) | ||
| #Copyright (C) 2006-2009 NVDA Contributors <http://www.nvda-project.org/> | ||
| #Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy | ||
| #This file is covered by the GNU General Public License. | ||
| #See the file COPYING for more details. | ||
|
|
||
| """Utilities to generate and play tones""" | ||
|
|
||
| import atexit | ||
| import nvwave | ||
| import config | ||
| import globalVars | ||
|
|
@@ -20,6 +21,13 @@ | |
| log.warning("Failed to initialize audio for tones") | ||
| player = None | ||
|
|
||
| # When exiting, ensure player is deleted before modules get cleaned up. | ||
| # Otherwise, WavePlayer.__del__ will fail with an exception. | ||
| @atexit.register | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is also a windows line ending here. |
||
| def _cleanup(): | ||
| global player | ||
| player = None | ||
|
|
||
| def beep(hz,length,left=50,right=50): | ||
| """Plays a tone at the given hz, length, and stereo balance. | ||
| @param hz: pitch in hz of the tone | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| #tests/unit/__init__.py | ||
| #A part of NonVisual Desktop Access (NVDA) | ||
| #This file is covered by the GNU General Public License. | ||
| #See the file COPYING for more details. | ||
| #Copyright (C) 2017 NV Access Limited | ||
|
|
||
| """NVDA unit testing. | ||
| All unit tests should reside within this package and should be | ||
| divided into modules and packages similar to the code they are testing. | ||
| Test modules must have a C{test_} prefix | ||
| and should contain one or more classes with a C{Test} prefix which subclass L{TestCase}. | ||
| Methods in test classes should have a C{test_} prefix. | ||
| """ | ||
|
|
||
| ### Ugly bootstrapping code. | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
| # The path to the unit tests. | ||
| UNIT_DIR = os.path.dirname(os.path.abspath(__file__)) | ||
| # The path to the top of the repo. | ||
| TOP_DIR = os.path.dirname(os.path.dirname(UNIT_DIR)) | ||
| # The path to the NVDA "source" directory. | ||
| SOURCE_DIR = os.path.join(TOP_DIR, "source") | ||
| # Let us import modules from the NVDA source. | ||
| sys.path.insert(1, SOURCE_DIR) | ||
| import sourceEnv | ||
|
|
||
| # Set options normally taken from the command line. | ||
| import globalVars | ||
| class AppArgs: | ||
| # The path from which to load a configuration file. | ||
| # Ideally, this would be an in-memory, default configuration. | ||
| # However, config currently requires a path. | ||
| # We use the unit test directory, since we want a clean config. | ||
| configPath = UNIT_DIR.decode("mbcs") | ||
| secure = False | ||
| disableAddons = True | ||
| globalVars.appArgs = AppArgs() | ||
|
|
||
| # We depend on the current directory to load some files; | ||
| # e.g. braille imports louis which loads liblouis.dll using a relative path. | ||
| os.chdir(SOURCE_DIR) | ||
| # The path to this package might be relative, so make it absolute, | ||
| # since we just changed directory. | ||
| __path__[0] = UNIT_DIR | ||
| # We don't want logging. | ||
| import logging | ||
| from logHandler import log | ||
| log.addHandler(logging.NullHandler()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, I think it could be helpful to be able to get the log information out of some tests. The log info could make debugging the unit tests easier. Could you open (and p4) an issue to keep track of the different ways we talked about to achieve this? I think for now there is low urgency on actually implementing this, however at the point when someone is trying to debug a unit test this could become more important. |
||
| # There's no point in logging anything at all, since it'll go nowhere. | ||
| log.setLevel(100) | ||
|
|
||
| # Much of this should eventually be replaced by stuff which gets reset before each test | ||
| # so the tests are isolated. | ||
| import config | ||
| config.initialize() | ||
| # Initialize languageHandler so that translatable strings work. | ||
| import languageHandler | ||
| languageHandler.setLanguage("en") | ||
| # NVDAObjects need appModuleHandler to be initialized. | ||
| import appModuleHandler | ||
| appModuleHandler.initialize() | ||
| # Anything which notifies of cursor updates requires braille to be initialized. | ||
| import braille | ||
| braille.initialize() | ||
| # The focus and navigator objects need to be initialized to something. | ||
| from NVDAObjects import NVDAObject | ||
| class PlaceholderNVDAObject(NVDAObject): | ||
| processID = None # Must be implemented to instantiate. | ||
| phObj = PlaceholderNVDAObject() | ||
| import api | ||
| api.setFocusObject(phObj) | ||
| api.setNavigatorObject(phObj) | ||
|
|
||
| # Stub speech functions to make them no-ops. | ||
| # Eventually, these should keep track of calls so we can make assertions. | ||
| import speech | ||
| speech.speak = lambda speechSequence, symbolLevel=None: None | ||
| speech.speakSpelling = lambda text, locale=None, useCharacterDescriptions=False: None | ||
|
|
||
| ### Base classes/utility functions. | ||
|
|
||
| import unittest | ||
|
|
||
| class TestCase(unittest.TestCase): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, I dont think we need this for now. We will need some way of managing setup/tear down of dependencies, but perhaps we should address this later. |
||
| """The base class which all test case classes should subclass. | ||
| """ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| ### | ||
| #This file is a part of the NVDA project. | ||
| #URL: http://www.nvaccess.org/ | ||
| #Copyright 2017 NV Access Limited. | ||
| #This program is free software: you can redistribute it and/or modify | ||
| #it under the terms of the GNU General Public License version 2.0, as published by | ||
| #the Free Software Foundation. | ||
| #This program is distributed in the hope that it will be useful, | ||
| #but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
| #This license can be found at: | ||
| #http://www.gnu.org/licenses/old-licenses/gpl-2.0.html | ||
| ### | ||
|
|
||
| import sys | ||
|
|
||
| Import("env") | ||
|
|
||
| cmd = [sys.executable, "-m", "unittest"] | ||
| tests = env["unitTests"] | ||
| if tests: | ||
| # Run specific tests. | ||
| tests = tests.split(",") | ||
| cmd += ["tests.unit." + test for test in tests] | ||
| else: | ||
| # Run all tests. | ||
| cmd.extend(("discover", "tests.unit", "-t", ".")) | ||
| target = env.Command(".", None, [cmd]) | ||
| Return('target') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| #tests/unit/test_cursorManager.py | ||
| #A part of NonVisual Desktop Access (NVDA) | ||
| #This file is covered by the GNU General Public License. | ||
| #See the file COPYING for more details. | ||
| #Copyright (C) 2017 NV Access Limited | ||
|
|
||
| """Unit tests for the cursorManager module. | ||
| """ | ||
|
|
||
| import unittest | ||
| import cursorManager | ||
| from . import TestCase | ||
| from .textProvider import BasicTextProvider | ||
|
|
||
| class CursorManager(cursorManager.CursorManager, BasicTextProvider): | ||
| """CursorManager which navigates within a provided string of text. | ||
| """ | ||
|
|
||
| class TestMove(TestCase): | ||
|
|
||
| def test_nextChar(self): | ||
| cm = CursorManager(text="abc") # Caret at "a" | ||
| cm.script_moveByCharacter_forward(None) | ||
| self.assertEqual(cm.selectionOffsets, (1, 1)) # Caret at "b" | ||
|
|
||
| def test_prevChar(self): | ||
| cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than the comment, the |
||
| cm.script_moveByCharacter_back(None) | ||
| self.assertEqual(cm.selectionOffsets, (0, 0)) # Caret at "a" | ||
|
|
||
| class TestSelection(TestCase): | ||
|
|
||
| def test_selectNextChar(self): | ||
| cm = CursorManager(text="abc") # Caret at "a" | ||
| cm.script_selectCharacter_forward(None) | ||
| self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected | ||
|
|
||
| def test_selectPrevChar(self): | ||
| cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b" | ||
| cm.script_selectCharacter_back(None) | ||
| self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected | ||
|
|
||
| def test_unselectPrevChar(self): | ||
| cm = CursorManager(text="abc") # Caret at "a" | ||
| cm.script_selectCharacter_forward(None) # "a" selected | ||
| cm.script_selectCharacter_back(None) # "a" unselected | ||
| self.assertEqual(cm.selectionOffsets, (0, 0)) # Caret at "a", no selection | ||
|
|
||
| def test_selForwardThenUnselThenSelBackward(self): | ||
| """Test selecting forward, then unselecting and selecting backward. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring could be more explicit about the dependencies between tests / behaviour. |
||
| """ | ||
| cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b" | ||
| cm.script_selectCharacter_forward(None) # "b" selected | ||
| cm.script_selectCharacter_back(None) # "b" unselected, caret at "b" | ||
| cm.script_selectCharacter_back(None) | ||
| self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected | ||
|
|
||
| def test_selectForwardThenSelBackward(self): | ||
| """Test selecting forward, then selecting backward without unselecting. | ||
| """ | ||
| cm = CursorManager(text="abc", selection=(1, 1)) # Caret at "b" | ||
| cm.script_selectCharacter_forward(None) # "b" selected | ||
| cm.script_selectWord_back(None) # "b" unselected, "a" selected | ||
| self.assertEqual(cm.selectionOffsets, (0, 1)) # "a" selected | ||
|
|
||
| class TestSelectAll(TestCase): | ||
| """Tests the select all command starting from different caret positions. | ||
| """ | ||
|
|
||
| def _selectAllTest(self, caret): | ||
| """Tests select all with the caret at the given offset. | ||
| """ | ||
| cm = CursorManager(text="abc", selection=(caret, caret)) | ||
| cm.script_selectAll(None) | ||
| self.assertEqual(cm.selectionOffsets, (0, 3)) # "abc" selected | ||
|
|
||
| def test_selectAllFromStart(self): | ||
| self._selectAllTest(0) # Caret at "a" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably fine for now. But in the future this might need to pulled into the working directory for the test (even better if it was in memory rather than moving actual files around) during the setup for the test. If all tests use the same config file, there is a chance that some test will modify the settings and we could get some pretty nasty test interactions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree that ideally, we want to keep this in memory and isolate it per test. If it eases your concern any in the short-term, it's worth noting that nothing in NVDA ever really saves the configuration except the save configuration command or auto save on NVDA exit, neither of which should ever be reasonably called from a unit test.