Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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.

13 changes: 10 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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%

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each of these lines ending with %sconsArgs% (lines: 76, 78, 80) has different line ending ^M

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
7 changes: 6 additions & 1 deletion nvdaHelper/ISimpleDOM_sconscript
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.
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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'])
7 changes: 6 additions & 1 deletion nvdaHelper/mathPlayer_sconscript
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.
Expand All @@ -21,5 +21,10 @@ tlbFile,headerFile,iidSourceFile,proxySourceFile,dlldataSourceFile=env.TypeLibra
source=idlFile,
MIDLFLAGS=['/I',Dir('.')],
)
# hack: Ignore midl.exe when deciding to rebuild, as its position in the dependencies
# 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'])
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:
exports=["env"])
env.Depends(target, sourceDir)
env.AlwaysBuild(target)
tests = env.Alias("tests", target)
10 changes: 6 additions & 4 deletions source/comInterfaces_sconscript
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 2013-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.
Expand Down Expand Up @@ -49,12 +49,14 @@ COM_INTERFACES = {
}

for k,v in COM_INTERFACES.iteritems():
>
targets=[Dir('comInterfaces').File(k),
# This buillds a .pyc file as well.
Dir('comInterfaces').File(k + "c")]
source=clsid=majorVersion=None
if isinstance(v,basestring):
env.comtypesInterface(target,v)
env.comtypesInterface(targets,v)
else:
env.comtypesInterface(target,Dir('comInterfaces').File('__init__.py'),clsid=v[0],majorVersion=v[1],minorVersion=v[2])
env.comtypesInterface(targets,Dir('comInterfaces').File('__init__.py'),clsid=v[0],majorVersion=v[1],minorVersion=v[2])

#When cleaning comInterfaces get rid of everything except for things starting with __ (e.g. __init__.py)
env.Clean(Dir('comInterfaces'),Glob('comInterfaces/[!_]*')+Glob('comInterfaces/_[!_]*'))
11 changes: 10 additions & 1 deletion source/nvwave.py
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.

Expand All @@ -11,6 +11,7 @@
from ctypes import *
from ctypes.wintypes import *
import time
import atexit
import wx
import winKernel
import wave
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
10 changes: 9 additions & 1 deletion source/tones.py
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
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
"""
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')
78 changes: 78 additions & 0 deletions tests/unit/test_cursorManager.py
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than the comment, the (1,1) could be held in a variable named caretAtB

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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"
Loading