Skip to content

Commit

Permalink
Add initial unit testing framework.
Browse files Browse the repository at this point in the history
Unit tests and infrastructure are located in the tests/unit directory. See the docstring in the tests/unit/__init__.py file for details.
You can run tests using "scons tests". See the "Running Tests" section of readme.md for details.
  • Loading branch information
jcsteh committed Mar 29, 2017
1 parent fc1e26e commit 7e21223
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ uninstaller/UAC.nsh
*.pyc
*.pyo
*.dmp
tests/unit/nvda.ini
21 changes: 21 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,24 @@ For example, to build a launcher with a specific version, you might type:
```
scons launcher version=test1
```

## Running Automated Tests
If you make a change to the NVDA code, you should run NVDA's automated tests.
These tests help to ensure that code changes do not unintentionally break functionality that was previously working.
Currently, NVDA has only one kind of automated testing: unit tests.

To run the unit tests, first change directory to the root of the NVDA source distribution as above.
Then, run:

```
scons tests
```

To run only specific tests, specify them using the `unitTests` variable on the command line.
The tests should be provided as a comma separated list.
Each test should be specified as a Python module, class or method relative to the `tests\unit` directory.
For example, to run only methods in the `TestMove` and `TestSelection` classes in the file `tests\unit\test_cursorManager.py` file, run this command:

```
scons tests unitTests=test_cursorManager.TestMove,test_cursorManager.TestSelection
```
7 changes: 7 additions & 0 deletions sconstruct
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ vars.Add("certTimestampServer", "The URL of the timestamping server to use to ti
vars.Add(PathVariable("outputDir", "The directory where the final built archives and such will be placed", "output",PathVariable.PathIsDirCreate))
vars.Add(ListVariable("nvdaHelperDebugFlags", "a list of debugging features you require", 'none', ["debugCRT","RTC","analyze"]))
vars.Add(EnumVariable('nvdaHelperLogLevel','The level of logging you wish to see, lower is more verbose','15',allowed_values=[str(x) for x in xrange(60)]))
if "tests" in COMMAND_LINE_TARGETS:
vars.Add("unitTests", "A list of unit tests to run", "")

#Base environment for this and sub sconscripts
env = Environment(variables=vars,HOST_ARCH='x86',tools=["textfile","gettext","t2t",keyCommandsDocTool,'doxygen','recursiveInstall'])
Expand Down Expand Up @@ -373,3 +375,8 @@ env.Alias("symbolsArchive", symbolsArchive)

env.Default(dist)

if "tests" in COMMAND_LINE_TARGETS:
target = env.SConscript("tests/unit/sconscript", exports=["env"])
env.Depends(target, sourceDir)
env.AlwaysBuild(target)
tests = env.Alias("tests", target)
Empty file added tests/__init__.py
Empty file.
89 changes: 89 additions & 0 deletions tests/unit/__init__.py
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())
# 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):
"""The base class which all test case classes should subclass.
"""
29 changes: 29 additions & 0 deletions tests/unit/sconscript
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')
64 changes: 64 additions & 0 deletions tests/unit/textProvider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#tests/unit/textProvider.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

"""Fake text provider implementation for testing of code which uses TextInfos.
See the L{BasicTextProvider} class.
"""

from NVDAObjects import NVDAObject, NVDAObjectTextInfo
import textInfos
from textInfos.offsets import Offsets

class BasicTextInfo(NVDAObjectTextInfo):
# NVDAHelper is not initialized, so we can't use Uniscribe.
useUniscribe = False

def _get_offsets(self):
return (self._startOffset, self._endOffset)

def updateCaret(self):
self.obj.selectionOffsets = (self._startOffset, self._startOffset)

def updateSelection(self):
self.obj.selectionOffsets = self.offsets

class BasicTextProvider(NVDAObject):
"""An NVDAObject which makes TextInfos based on a provided string of text.
Example usage:
>>> obj = BasicTextProvider(text="abcd")
>>> ti = obj.makeTextInfo(textInfos.POSITION_CARET)
>>> ti.offsets
(0, 0)
>>> ti.expand(textInfos.UNIT_CHARACTER)
>>> ti.text
"a"
>>> ti.offsets
(0, 1)
>>> ti.updateSelection()
>>> obj.selectionOffsets
(0, 1)
"""

processID = None # Must be implemented to instantiate.
TextInfo = BasicTextInfo

def __init__(self, text=None, selection=(0, 0)):
"""
@param text: The text to provide via TextInfos.
@type text: basestring
@param selection: The start and end offsets of the initial selection;
same start and end is caret with no selection.
@type selection: tuple of (int, int)
"""
super(BasicTextProvider, self).__init__()
self.basicText = unicode(text)
self.selectionOffsets = selection

def makeTextInfo(self, position):
if position in (textInfos.POSITION_CARET, textInfos.POSITION_SELECTION):
start, end = self.selectionOffsets
position = Offsets(start, end)
return super(BasicTextProvider, self).makeTextInfo(position)

0 comments on commit 7e21223

Please sign in to comment.