Full Code of bfirsh/needle for AI

master c2d28ee07278 cached
32 files
68.9 KB
17.9k tokens
113 symbols
1 requests
Download .txt
Repository: bfirsh/needle
Branch: master
Commit: c2d28ee07278
Files: 32
Total size: 68.9 KB

Directory structure:
gitextract_ixilxv5m/

├── .gitignore
├── .travis.yml
├── CHANGES.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.txt
│   ├── make.bat
│   └── requirements.txt
├── needle/
│   ├── __init__.py
│   ├── cases.py
│   ├── driver.py
│   ├── engines/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── imagemagick_engine.py
│   │   ├── perceptualdiff_engine.py
│   │   └── pil_engine.py
│   ├── js/
│   │   └── __init__.py
│   └── plugin.py
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── plugin_test_cases/
│   │   └── red_box.py
│   ├── test_diff.py
│   ├── test_driver.py
│   ├── test_imagemagick_engine.py
│   ├── test_in_memory.py
│   ├── test_perceptualdiff_engine.py
│   ├── test_pil_engine.py
│   └── test_plugin.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pyc
*.egg-info
.DS_Store
*~
.*.swo
.*.swp
.*.swn
.*.swm
/.tox/
/build/
/dist/
selenium-server-standalone-*.jar
/docs/_build
tests/screenshots/*.png
geckodriver.log
ghostdriver.log
__pycache__


================================================
FILE: .travis.yml
================================================
language: python
sudo: required
dist: trusty
python:
  - "2.7"
  - "3.2"
  - "3.3"
  - "3.4"
  - "3.5"
  - "3.6"
# Firefox 48+ only works with geckodriver, first supported properly in selenium 3.0.0.b3
# See https://github.com/SeleniumHQ/selenium/issues/2739#issuecomment-249482533
env:
  - SELENIUM_VERSION='<3' NEEDLE_BROWSER=chrome
  - SELENIUM_VERSION='<4' NEEDLE_BROWSER=chrome
  - SELENIUM_VERSION='<4' NEEDLE_BROWSER=firefox
  - SELENIUM_VERSION='<3' NEEDLE_BROWSER=phantomjs
  - SELENIUM_VERSION='<4' NEEDLE_BROWSER=phantomjs
before_install:
  - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then export DISPLAY=:99.0; fi
  - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then sh -e /etc/init.d/xvfb start; fi
  - if [[ "$NEEDLE_BROWSER" != "phantomjs" ]]; then sleep 3; fi # give xvfb some time to start
  - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then wget https://github.com/mozilla/geckodriver/releases/download/v0.15.0/geckodriver-v0.15.0-linux64.tar.gz; fi
  - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then mkdir geckodriver; fi
  - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then tar -xzf geckodriver-v0.15.0-linux64.tar.gz -C geckodriver; fi
  - if [[ "$NEEDLE_BROWSER" == "firefox" ]]; then export PATH=$PATH:$PWD/geckodriver; fi
  - sudo apt-get update -qq
  - sudo apt-get install -y perceptualdiff imagemagick
  - if [[ "$NEEDLE_BROWSER" == "chrome" ]]; then sudo apt-get install -y chromium-chromedriver; fi
  - if [[ "$NEEDLE_BROWSER" == "chrome" ]]; then export PATH=$PATH:/usr/lib/chromium-browser/; fi
addons:
  firefox: '52.0'
install:
  - pip install "selenium ${SELENIUM_VERSION}"
  - pip install -e .
script:
  - nosetests -v


================================================
FILE: CHANGES.md
================================================
Change log
==========

Upcoming 0.5.0
--------------

- Dropped Python 2.6 support.
- Verified support for Python 3.2-3.6.
- Firefox improvements: use FirefoxWebElement and geckodriver by default.

0.4.1 (2017-01-04)
------------------

- Fixed a race condition in creating baseline and output directories.

0.4.0 (2016-12-21)
------------------

- Added support for Selenium 3.
- Added ImageMagick engine.
- Fixed an issue with false positives yielded by the Perceptual engine.

0.3.0 (2015-02-22)
------------------

- Added the cleanup_on_success option for deleting screenshots after successful test runs.

0.2.4 (2015-02-08)
------------------

- Use selenium native element location instead of javascript call in order to assure crossplatform compatability.

0.2.3 (2015-01-17)
------------------

- Fixed a unicode error (issue #23).

0.2.2 (2014-08-16)
------------------

- Fixed an issue with saving screenshots on Windows.

0.2.1 (2014-04-10)
------------------

 - Fixed a regression in the PIL engine.

0.2.0 (2014-04-07)
------------------

 - Added support for Python 3.
 - Changed default threshold from 0.1 to 0.
 - Added configurable way of plugging external diff engines like PerceptualDiff.
 - Removed the necessity to run the Selenium server by using a Firefox web
   driver instance by default. This is slightly backwards-incompatible if you
   relied on the now-removed `driver_command_executor`,
   `driver_desired_capabilities` and `driver_browser_profile` attributes.
   To control the logic for selecting the proper web driver, you may simply
   override the `get_web_driver()` method.
 - The `--with-needle-capture` and `NeedleTestCase.capture` options were
   deprecated and will be removed in version 0.4.0. Instead, you should now
   respectively use the new, more explicit `--with-save-baseline` and
   `NeedleTestCase.save_baseline` options. Note that those new options will
   systematically cause the baseline image files to be saved on disk,
   overwriting potentially existing baseline files.
 - Removed the `NeedleWebElement.get_computed_property()` method. Instead, you
   may use Selenium's built-in `value_of_css_property()` method.
 - Upgraded vendored jQuery to version 11.0.

0.1.0 (2014-02-20)
------------------

 - Add `set_viewport_size()` method to `NeedleTestCase`
 - Calculate the dimensions of elements more accurately with jQuery
 - Only load jQuery if it hasn't already been loaded

Thanks @jphalip!

0.0.2 (2013-10-24)
------------------

 - Allow needle to be used with custom web driver
 - Replace PIL with pillow

Thanks @treyhunner!

0.0.1 (2013-05-07)
------------------

Initial release.



================================================
FILE: LICENSE
================================================
Copyright (c) 2011, Ben Firshman
All rights reserved.
 
Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met:
 
 * Redistributions of source code must retain the above copyright notice, this 
   list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
   this list of conditions and the following disclaimer in the documentation 
   and/or other materials provided with the distribution.
 * The names of its contributors may not be used to endorse or promote products 
   derived from this software without specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



================================================
FILE: MANIFEST.in
================================================
include LICENSE
include *.md
recursive-include needle/js *


================================================
FILE: README.md
================================================
Needle
======

[![Build Status](https://travis-ci.org/python-needle/needle.png?branch=master)](https://travis-ci.org/python-needle/needle)

Needle is a tool for testing visuals with [Selenium](http://seleniumhq.org/) 
and [nose](https://nose.readthedocs.io/).

It checks that visuals (CSS/fonts/images/SVG/etc.) render correctly by taking
screenshots of portions of a website and comparing them against known good
screenshots. It also provides tools for testing calculated CSS values and the
position of HTML elements.

Example
-------

This is what a Needle test case looks like:

```python
from needle.cases import NeedleTestCase

class BBCNewsTest(NeedleTestCase):
    def test_masthead(self):
        self.driver.get('http://www.bbc.co.uk/news/')
        self.assertScreenshot('#blq-mast', 'bbc-masthead')
```

This example checks for regressions in the appearance of the BBC's masthead.

Documentation
-------------

Full documentation available on [Read the Docs](https://needle.readthedocs.io/).

If you'd like to build the documentation yourself, first install ``sphinx``:

    pip install sphinx
    
Then run:

    cd docs
    make html
    
The documentation will then be available browsable from
``docs/_build/index.html``.

Running Needle's test suite
---------------------------

First install tox (usually via ``pip install tox``).  Then:

    $ tox


================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
PAPER         =
BUILDDIR      = _build

# Internal variables.
PAPEROPT_a4     = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest

help:
	@echo "Please use \`make <target>' where <target> is one of"
	@echo "  html       to make standalone HTML files"
	@echo "  dirhtml    to make HTML files named index.html in directories"
	@echo "  singlehtml to make a single large HTML file"
	@echo "  pickle     to make pickle files"
	@echo "  json       to make JSON files"
	@echo "  htmlhelp   to make HTML files and a HTML help project"
	@echo "  qthelp     to make HTML files and a qthelp project"
	@echo "  devhelp    to make HTML files and a Devhelp project"
	@echo "  epub       to make an epub"
	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
	@echo "  text       to make text files"
	@echo "  man        to make manual pages"
	@echo "  changes    to make an overview of all changed/added/deprecated items"
	@echo "  linkcheck  to check all external links for integrity"
	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"

clean:
	-rm -rf $(BUILDDIR)/*

html:
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

dirhtml:
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."

singlehtml:
	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
	@echo
	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."

pickle:
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
	@echo
	@echo "Build finished; now you can process the pickle files."

json:
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
	@echo
	@echo "Build finished; now you can process the JSON files."

htmlhelp:
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
	@echo
	@echo "Build finished; now you can run HTML Help Workshop with the" \
	      ".hhp project file in $(BUILDDIR)/htmlhelp."

qthelp:
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
	@echo
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Needle.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Needle.qhc"

devhelp:
	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
	@echo
	@echo "Build finished."
	@echo "To view the help file:"
	@echo "# mkdir -p $$HOME/.local/share/devhelp/Needle"
	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Needle"
	@echo "# devhelp"

epub:
	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
	@echo
	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."

latex:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
	@echo "Run \`make' in that directory to run these through (pdf)latex" \
	      "(use \`make latexpdf' here to do that automatically)."

latexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through pdflatex..."
	make -C $(BUILDDIR)/latex all-pdf
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

text:
	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
	@echo
	@echo "Build finished. The text files are in $(BUILDDIR)/text."

man:
	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
	@echo
	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."

changes:
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
	@echo
	@echo "The overview file is in $(BUILDDIR)/changes."

linkcheck:
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
	@echo
	@echo "Link check complete; look for any errors in the above output " \
	      "or in $(BUILDDIR)/linkcheck/output.txt."

doctest:
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
	@echo "Testing of doctests in the sources finished, look at the " \
	      "results in $(BUILDDIR)/doctest/output.txt."


================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# Needle documentation build configuration file, created by
# sphinx-quickstart on Tue Apr  5 19:53:10 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

import sys, os

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))

# -- General configuration -----------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix of source filenames.
source_suffix = '.txt'

# The encoding of source files.
#source_encoding = 'utf-8-sig'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'Needle'
copyright = u'2011, Ben Firshman'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1a1'
# The full version, including alpha/beta/rc tags.
release = '0.1a1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None

# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']

# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None

# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []


# -- Options for HTML output ---------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
html_theme = 'nature'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}

# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []

# The name for this set of Sphinx documents.  If None, it defaults to
# "<project> v<release> documentation".
#html_title = None

# A shorter title for the navigation bar.  Default is the same as html_title.
#html_short_title = None

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None

# The name of an image file (within the static path) to use as favicon of the
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'

# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True

# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}

# If false, no module index is generated.
#html_domain_indices = True

# If false, no index is generated.
#html_use_index = True

# If true, the index is split into individual pages for each letter.
#html_split_index = False

# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it.  The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''

# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None

# Output file base name for HTML help builder.
htmlhelp_basename = 'Needledoc'


# -- Options for LaTeX output --------------------------------------------------

# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'

# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
  ('index', 'Needle.tex', u'Needle Documentation',
   u'Ben Firshman', 'manual'),
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False

# If true, show page references after internal links.
#latex_show_pagerefs = False

# If true, show URL addresses after external links.
#latex_show_urls = False

# Additional stuff for the LaTeX preamble.
#latex_preamble = ''

# Documents to append as an appendix to all manuals.
#latex_appendices = []

# If false, no module index is generated.
#latex_domain_indices = True


# -- Options for manual page output --------------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    ('index', 'needle', u'Needle Documentation',
     [u'Ben Firshman'], 1)
]


================================================
FILE: docs/index.txt
================================================
Needle: Automated tests for your visuals
========================================

Needle is a tool for testing your CSS and visuals with `Selenium <http://seleniumhq.org/>`_
and `nose <https://nose.readthedocs.io/>`_.

It checks that visuals (CSS/fonts/images/SVG/etc.) render correctly by taking
screenshots of portions of a website and comparing them against known good
screenshots. It also provides tools for testing calculated CSS values and the
position of HTML elements.

Installation
------------

If you haven't got `pip <http://www.pip-installer.org/>`_ installed::

    $ sudo easy_install pip

As root, or in a `virtualenv <http://www.virtualenv.org/>`_::

    $ pip install selenium
    $ pip install needle


Getting started
---------------

Create ``test_bbc.py`` in an empty directory::

    from needle.cases import NeedleTestCase

    class BBCNewsTest(NeedleTestCase):
        def test_masthead(self):
            self.driver.get('http://www.bbc.co.uk/news/')
            self.assertScreenshot('#blq-mast', 'bbc-masthead')

This is a test case which tells the Selenium web driver (by default Firefox)
to open BBC News and check the bar across the top of the page looks correct.
:py:meth:`~needle.cases.NeedleTestCase.assertScreenshot` take two arguments: a
CSS selector for the element we are capturing and a filename for the image.

To create an initial screenshot of the logo, we need to run Needle in
'baseline saving' mode::

    $ nosetests test_bbc.py --with-save-baseline

This will create ``screenshots/baseline/bbc-masthead.png``. Open it up and
check it looks okay.

Now if we run our tests, it will take the same screenshot and check it against
the screenshot on disk::

    $ nosetests test_bbc.py

If a regression in your CSS causes them to become significantly different, the
test will fail.


Selecting a WebDriver
---------------------

You may control which browser is used by Needle by overriding the
``get_web_driver()`` method::

    from needle.cases import NeedleTestCase
    from needle.driver import NeedlePhantomJS

    class MyTests(NeedleTestCase):

        @classmethod
        def get_web_driver(cls):
            return NeedlePhantomJS()

        def test_something(self):
            ...

By default Needle uses ``NeedleFirefox``, which is a wrapper of Selenium's
built-in ``selenium.webdriver.firefox.webdriver.WebDriver`` class. You may use
any of the following WebDrivers: ``NeedleRemote``, ``NeedlePhantomJS``,
``NeedleFirefox``, ``NeedleChrome``, ``NeedleIe``, ``NeedleOpera`` and
``NeedleSafari``. Refer to Selenium's documentation to learn how to install and
configure any of those WebDrivers.


Setting the viewport's size
---------------------------

You may set the size of the browser's viewport using the
``set_viewport_size()`` method::

    from needle.cases import NeedleTestCase

    class MyTests(NeedleTestCase):

        def test_something(self):
            self.set_viewport_size(width=1024, height=768)
            ...

This is particularly useful to predict the size of the resulting screenshots
when taking fullscreen captures, or to test responsive sites.

You may also set the default viewport size for all your tests with the
``viewport_width`` and ``viewport_height`` class attributes::

    from needle.cases import NeedleTestCase

    class MyTests(NeedleTestCase):
        viewport_width = 1024
        viewport_height = 768

        ...

Engines
-------

By default Needle uses the PIL engine (``needle.engines.pil_engine.Engine``)
to take screenshots. Instead of PIL, you may also use PerceptualDiff or
ImageMagick.

Example with PerceptualDiff::

    from needle.cases import NeedleTestCase

    class MyTests(NeedleTestCase):
        engine_class = 'needle.engines.perceptualdiff_engine.Engine'

        def test_something(self):
            ...

Example with ImageMagick::

    from needle.cases import NeedleTestCase

    class MyTests(NeedleTestCase):
        engine_class = 'needle.engines.imagemagick_engine.Engine'

        def test_something(self):
            ...

Besides being much faster than PIL, PerceptualDiff and ImageMagick also
generate a diff PNG file when a test fails, highlighting the differences
between the baseline image and the new screenshot.

Note that to use the PerceptualDiff engine you will first need to
`download <http://pdiff.sourceforge.net/>`_ the ``perceptualdiff`` binary and
place it in your ``PATH``.

To use the ImageMagick engine you will need to install a package on your
machine (e.g. ``sudo apt-get install imagemagick`` on Ubuntu or
``brew install imagemagick`` on OSX).

File cleanup
------------

Each time you run tests, Needle will create new screenshot images on disk, for
comparison with the baseline screenshots. It's then up to you whether you want
to delete them or archive them.

Set the ``cleanup_on_success`` class attribute to ``True`` to delete these
files for all successful tests. Any screenshots that differ from the baseline
will remain on disk for your inspection. Example::

    from needle.cases import NeedleTestCase

    class MyTests(NeedleTestCase):
        cleanup_on_success = True

        def test_something(self):
            ...

By default, ``cleanup_on_success`` is ``False``.

You may also activate the file cleanup from the command line by using the
``--with-needle-cleanup-on-success`` nose plugin:

    $ nosetests --with-needle-cleanup-on-success

Indices and tables
------------------

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`



================================================
FILE: docs/make.bat
================================================
@ECHO OFF

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
	set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)

if "%1" == "" goto help

if "%1" == "help" (
	:help
	echo.Please use `make ^<target^>` where ^<target^> is one of
	echo.  html       to make standalone HTML files
	echo.  dirhtml    to make HTML files named index.html in directories
	echo.  singlehtml to make a single large HTML file
	echo.  pickle     to make pickle files
	echo.  json       to make JSON files
	echo.  htmlhelp   to make HTML files and a HTML help project
	echo.  qthelp     to make HTML files and a qthelp project
	echo.  devhelp    to make HTML files and a Devhelp project
	echo.  epub       to make an epub
	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
	echo.  text       to make text files
	echo.  man        to make manual pages
	echo.  changes    to make an overview over all changed/added/deprecated items
	echo.  linkcheck  to check all external links for integrity
	echo.  doctest    to run all doctests embedded in the documentation if enabled
	goto end
)

if "%1" == "clean" (
	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
	del /q /s %BUILDDIR%\*
	goto end
)

if "%1" == "html" (
	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
	goto end
)

if "%1" == "dirhtml" (
	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
	goto end
)

if "%1" == "singlehtml" (
	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
	goto end
)

if "%1" == "pickle" (
	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can process the pickle files.
	goto end
)

if "%1" == "json" (
	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can process the JSON files.
	goto end
)

if "%1" == "htmlhelp" (
	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
	goto end
)

if "%1" == "qthelp" (
	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Needle.qhcp
	echo.To view the help file:
	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Needle.ghc
	goto end
)

if "%1" == "devhelp" (
	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished.
	goto end
)

if "%1" == "epub" (
	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The epub file is in %BUILDDIR%/epub.
	goto end
)

if "%1" == "latex" (
	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
	goto end
)

if "%1" == "text" (
	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The text files are in %BUILDDIR%/text.
	goto end
)

if "%1" == "man" (
	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The manual pages are in %BUILDDIR%/man.
	goto end
)

if "%1" == "changes" (
	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
	if errorlevel 1 exit /b 1
	echo.
	echo.The overview file is in %BUILDDIR%/changes.
	goto end
)

if "%1" == "linkcheck" (
	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
	if errorlevel 1 exit /b 1
	echo.
	echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
	goto end
)

if "%1" == "doctest" (
	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
	if errorlevel 1 exit /b 1
	echo.
	echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
	goto end
)

:end


================================================
FILE: docs/requirements.txt
================================================
# Used by ReadTheDocs for documentation builds
Sphinx==1.5.3


================================================
FILE: needle/__init__.py
================================================
__version__ = '0.5.0'
__author__ = 'Ben Firshman'
__contact__ = 'ben@firshman.co.uk'
__homepage__ = 'https://github.com/python-needle/needle'



================================================
FILE: needle/cases.py
================================================
# encoding: utf-8
from __future__ import absolute_import
from __future__ import print_function

from warnings import warn
from contextlib import contextmanager
from errno import EEXIST
import os
import sys
import time

if sys.version_info > (2, 7):
    from unittest import TestCase
else:
    from unittest2 import TestCase

if sys.version_info >= (3, 0):
    basestring = str


from PIL import Image

from selenium.common.exceptions import WebDriverException

from needle.engines.pil_engine import ImageDiff
from needle.driver import (NeedleFirefox, NeedleChrome, NeedleIe, NeedleOpera,
                           NeedleSafari, NeedlePhantomJS, NeedleWebElementMixin)

DRIVER_ACQUISITION_TIMEOUT = 5  # seconds


def _object_filename(obj):
    return os.path.abspath(sys.modules[type(obj).__module__].__file__)


def import_from_string(path):
    """
    Utility function to dynamically load a class specified by a string,
    e.g. 'path.to.my.Class'.
    """
    module_name, klass = path.rsplit('.', 1)
    module = __import__(module_name, fromlist=[klass])
    return getattr(module, klass)


class NeedleTestCase(TestCase):
    """
    A `unittest2 <http://www.voidspace.org.uk/python/articles/unittest2.shtml>`_
    test case which provides tools for testing CSS with Selenium.
    """

    driver = None

    capture = False  # Deprecated
    save_baseline = False
    cleanup_on_success = False

    viewport_width = 1024
    viewport_height = 768

    output_directory = None
    baseline_directory = None

    engine_class = 'needle.engines.pil_engine.Engine'

    @classmethod
    def setUpClass(cls):
        if os.environ.get('NEEDLE_CAPTURE'):
            cls.capture = True
        if os.environ.get('NEEDLE_SAVE_BASELINE'):
            cls.save_baseline = True
        if os.environ.get('NEEDLE_CLEANUP_ON_SUCCESS'):
            cls.cleanup_on_success = True

        # Instantiate the diff engine
        klass = import_from_string(cls.engine_class)
        cls.engine = klass()

        cls.driver = cls.get_web_driver()
        cls.driver.set_window_position(0, 0)
        cls.set_viewport_size(cls.viewport_width, cls.viewport_height)
        super(NeedleTestCase, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        if isinstance(cls.driver, NeedlePhantomJS):
            # Workaround for https://github.com/SeleniumHQ/selenium/issues/767
            cls.driver.service.send_remote_shutdown_command()
            cls.driver.service._cookie_temp_file = None
        cls.driver.quit()
        super(NeedleTestCase, cls).tearDownClass()

    @classmethod
    def get_web_driver(cls):
        """
        Returns the WebDriver instance to be used. Defaults to `NeedleFirefox()`.
        Override this method if you'd like to control the logic for choosing
        the proper WebDriver instance.
        """
        browser_name = os.environ.get('NEEDLE_BROWSER')
        browser_map = {
            'firefox': NeedleFirefox,
            'chrome': NeedleChrome,
            'ie': NeedleIe,
            'opera': NeedleOpera,
            'safari': NeedleSafari,
            'phantomjs': NeedlePhantomJS,
        }
        browser_class = browser_map.get(browser_name, NeedleFirefox)
        # Allow a few retries to get the driver, in case it isn't quite ready yet
        start_time = time.time()
        while True:
            try:
                browser = browser_class()
                break
            except Exception as e:
                if (not isinstance(e, WebDriverException)) and e.__class__.__name__ != 'WebDriverException':
                    # nose likes to change selenium's WebDriverException to "nose.proxy.WebDriverException"
                    raise
                if time.time() - start_time >= DRIVER_ACQUISITION_TIMEOUT:
                    raise
                time.sleep(1)
        return browser

    def __init__(self, *args, **kwargs):
        super(NeedleTestCase, self).__init__(*args, **kwargs)
        # TODO: should output directory be timestamped?
        if self.output_directory is None:
            self.output_directory = os.environ.get('NEEDLE_OUTPUT_DIR', os.path.realpath(os.path.join(os.getcwd(), 'screenshots')))
        # TODO: Should baseline be a top-level peer to output_directory?
        if self.baseline_directory is None:
            self.baseline_directory = os.environ.get('NEEDLE_BASELINE_DIR', os.path.realpath(os.path.join(os.getcwd(), 'screenshots', 'baseline')))

        # Create the output and baseline directories if they do not yet exist.
        for dirname in (self.baseline_directory, self.output_directory):
            # Recursively create the directory, handling its
            # prior existence as a valid exception.
            # This will guard against race conditions.
            # E.g. when running tests in multithreaded mode
            # they likely have the same directories specified
            # and might encounter this block at the same time.
            try:
                os.makedirs(dirname)
            except OSError as err:
                if err.errno == EEXIST and os.path.isdir(dirname):
                    pass
                else:
                    raise

    @classmethod
    def set_viewport_size(cls, width, height):
        cls.driver.set_window_size(width, height)

        # Measure the difference between the actual document width and the
        # desired viewport width so we can account for scrollbars:
        measured = cls.driver.execute_script("return {width: document.body.clientWidth, height: document.body.clientHeight};")
        delta = width - measured['width']

        cls.driver.set_window_size(width + delta, height)

    def assertScreenshot(self, element_or_selector, file, threshold=0):
        """assert-style variant of compareScreenshot context manager

        compareScreenshot() can be considerably more efficient for recording baselines by avoiding the need
        to load pages before checking whether we're actually going to save them. This function allows you
        to continue using normal unittest-style assertions if you don't need the efficiency benefits
        """

        with self.compareScreenshot(element_or_selector, file, threshold=threshold):
            pass

    @contextmanager
    def compareScreenshot(self, element_or_selector, file, threshold=0):
        """
        Assert that a screenshot of an element is the same as a screenshot on disk,
        within a given threshold.

        :param element_or_selector:
            Either a CSS selector as a string or a
            :py:class:`~needle.driver.NeedleWebElementMixin` object that represents
            the element to capture.
        :param file:
            If a string, then assumed to be the filename for the screenshot,
            which will be appended with ``.png``. Otherwise assumed to be
            a file object for the baseline image.
        :param threshold:
            The threshold for triggering a test failure.
        """

        yield  # To allow using this method as a context manager

        if not isinstance(element_or_selector, NeedleWebElementMixin):
            element = self.driver.find_element_by_css_selector(element_or_selector)
        else:
            element = element_or_selector

        if not isinstance(file, basestring):
            # Comparing in-memory files instead of on-disk files
            baseline_image = Image.open(file).convert('RGB')
            fresh_screenshot = element.get_screenshot()
            diff = ImageDiff(fresh_screenshot, baseline_image)
            distance = abs(diff.get_distance())
            if distance > threshold:
                raise AssertionError("The new screenshot did not match "
                                     "the baseline (by a distance of %.2f)"
                                     % distance)
        else:
            baseline_file = os.path.join(self.baseline_directory, '%s.png' % file)
            output_file = os.path.join(self.output_directory, '%s.png' % file)

            # Determine whether we should save the baseline image
            save_baseline = False
            if self.save_baseline:
                save_baseline = True
            elif self.capture:
                warn("The 'NeedleTestCase.capture' attribute and '--with-save-baseline' nose option "
                     "are deprecated since version 0.2.0. Use 'save_baseline' and '--with-save-baseline' "
                     "instead. See the changelog for more information.",
                     PendingDeprecationWarning)
                if os.path.exists(baseline_file):
                    self.skipTest('Not capturing %s, its baseline image already exists. If you '
                                  'want to capture this element again, delete %s'
                                  % (file, baseline_file))
                else:
                    save_baseline = True

            if save_baseline:
                # Save the baseline screenshot and bail out
                element.get_screenshot().save(baseline_file)
                return
            else:
                if not os.path.exists(baseline_file):
                    raise IOError('The baseline screenshot %s does not exist. '
                                  'You might want to re-run this test in baseline-saving mode.'
                                  % baseline_file)

                # Save the new screenshot
                element.get_screenshot().save(output_file)

                try:
                    self.engine.assertSameFiles(output_file, baseline_file, threshold)
                except:
                    raise
                else:
                    if self.cleanup_on_success:
                        os.remove(output_file)


================================================
FILE: needle/driver.py
================================================
# encoding: utf-8
from __future__ import absolute_import

import base64
import os
import sys

if sys.version_info >= (3, 0):
    from urllib.parse import quote
    from io import BytesIO as IOClass
else:
    from urllib import quote
    try:
        from cStringIO import StringIO as IOClass
    except ImportError:
        from StringIO import StringIO as IOClass


from PIL import Image

from selenium.common.exceptions import WebDriverException
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.firefox.webdriver import WebDriver as Firefox
from selenium.webdriver.chrome.webdriver import WebDriver as Chrome
from selenium.webdriver.ie.webdriver import WebDriver as Ie
from selenium.webdriver.opera.webdriver import WebDriver as Opera
from selenium.webdriver.safari.webdriver import WebDriver as Safari
from selenium.webdriver.phantomjs.webdriver import WebDriver as PhantomJS
from selenium.webdriver.remote.webdriver import WebDriver as Remote

try:
    # Added in selenium 3.0.0.b3
    from selenium.webdriver.firefox.webelement import FirefoxWebElement
except ImportError:
    from selenium.webdriver.remote.webelement import WebElement as FirefoxWebElement


class NeedleWebElementMixin(object):
    """
    An element on a page that Selenium has opened.

    It is a Selenium :py:class:`~selenium.webdriver.remote.webelement.WebElement`
    object with some extra methods for testing CSS.
    """
    def get_dimensions(self):
        """
        Returns a dictionary containing, in pixels, the element's ``width`` and
        ``height``, and it's ``left`` and ``top`` position relative to the document.
        """
        location = self.location
        size = self.size
        return {
            "top": location['y'],
            "left": location['x'],
            "width": size['width'],
            "height": size['height']
        }

    def get_screenshot(self):
        """
        Returns a screenshot of this element as a PIL image.
        """
        d = self.get_dimensions()
        
        # Cast values to int in order for _ImageCrop not to break
        d['left'] = int(d['left'])
        d['top'] = int(d['top'])
        d['width'] = int(d['width'])
        d['height'] = int(d['height'])

        try:
            # For selenium >= 2.46.1, W3C WebDriver spec drivers (like geckodriver)
            fh = IOClass(self.screenshot_as_png)
            image = Image.open(fh).convert('RGB')
            # Make sure it isn't actually a full-page screenshot (PhantomJS)
            if image.size == (d['width'], d['height']):
                return image
        except (AttributeError, WebDriverException):
            # Fall back to cropping a full page screenshot
            image = self._parent.get_screenshot_as_image()

        return image.crop((
            d['left'],
            d['top'],
            d['left'] + d['width'],
            d['top'] + d['height'],
        ))


class NeedleWebDriverMixin(object):
    """
    Selenium WebDriver mixin with some extra methods for testing CSS.
    """
    def load_html(self, html):
        """
        Similar to :py:meth:`get`, but instead of passing a URL to load in the
        browser, the HTML for the page is provided.
        """
        self.get('data:text/html,' + quote(html))

    def get_screenshot_as_image(self):
        """
        Returns a screenshot of the current page as an RGB
        `PIL image <http://www.pythonware.com/library/pil/handbook/image.htm>`_.
        """
        fh = IOClass(base64.b64decode(self.get_screenshot_as_base64().encode('ascii')))
        return Image.open(fh).convert('RGB')

    def load_jquery(self):
        """
        Loads jQuery onto the current page so calls to
        :py:meth:`execute_script` have access to it.
        """
        if (self.execute_script('return typeof(jQuery)') == 'undefined'):
            self.execute_script(open(
                os.path.join(self._get_js_path(), 'jquery-1.11.0.min.js')
            ).read() + '\nreturn "";')

    def _get_js_path(self):
        return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'js')

    def create_web_element(self, element_id, *args, **kwargs):
        if isinstance(self, NeedleFirefox):
            return NeedleFirefoxWebElement(self, element_id, w3c=self.w3c, *args, **kwargs)
        else:
            return NeedleWebElement(self, element_id, w3c=self.w3c, *args, **kwargs)


class NeedleRemote(NeedleWebDriverMixin, Remote):
    """
    The same as Selenium's remote WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedlePhantomJS(NeedleWebDriverMixin, PhantomJS):
    """
    The same as Selenium's PhantomJS WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedleFirefox(NeedleWebDriverMixin, Firefox):
    """
    The same as Selenium's Firefox WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedleChrome(NeedleWebDriverMixin, Chrome):
    """
    The same as Selenium's Chrome WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedleIe(NeedleWebDriverMixin, Ie):
    """
    The same as Selenium's Internet Explorer WebDriver, but with
    NeedleWebDriverMixin's functionality.
    """

class NeedleOpera(NeedleWebDriverMixin, Opera):
    """
    The same as Selenium's Opera WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedleSafari(NeedleWebDriverMixin, Safari):
    """
    The same as Selenium's Safari WebDriver, but with NeedleWebDriverMixin's
    functionality.
    """

class NeedleWebElement(NeedleWebElementMixin, WebElement):
    """
    The same as Selenium's WebElement, but with NeedleWebElementMixin's
    functionality.
    """

class NeedleFirefoxWebElement(NeedleWebElementMixin, FirefoxWebElement):
    """
    The same as Selenium's FirefoxWebElement, but with NeedleWebElementMixin's
    functionality.
    """


================================================
FILE: needle/engines/__init__.py
================================================


================================================
FILE: needle/engines/base.py
================================================
class EngineBase(object):
    """
    Base class for diff engines.
    """

    def assertSameFiles(self, output_file, baseline_file, threshold):
        raise NotImplementedError

================================================
FILE: needle/engines/imagemagick_engine.py
================================================
import os
import subprocess

from needle.engines.base import EngineBase


class Engine(EngineBase):
    compare_path = "compare"
    compare_command = ("{compare} -metric RMSE -subimage-search -dissimilarity-threshold 1.0 {baseline} "
                       "{new} {diff}")

    def assertSameFiles(self, output_file, baseline_file, threshold=0):
        diff_file = output_file.replace('.png', '.diff.png')

        compare_cmd = self.compare_command.format(
            compare=self.compare_path,
            baseline=baseline_file,
            new=output_file,
            diff=diff_file)
        process = subprocess.Popen(compare_cmd, shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        compare_stdout, compare_stderr = process.communicate()

        difference = float(compare_stderr.split()[1][1:-1])
        if difference <= threshold:
            os.remove(diff_file)
            return

        raise AssertionError("The new screenshot '{new}' did not match "
                             "the baseline '{baseline}' (See {diff}):\n"
                             "{stdout}{stderr}"
                             .format(new=output_file,
                                     baseline=baseline_file,
                                     diff=diff_file,
                                     stdout=compare_stdout,
                                     stderr=compare_stderr))

================================================
FILE: needle/engines/perceptualdiff_engine.py
================================================
import subprocess
import os

from PIL import Image

from needle.engines.base import EngineBase


class Engine(EngineBase):

    perceptualdiff_path = 'perceptualdiff'
    perceptualdiff_output_png = True

    def assertSameFiles(self, output_file, baseline_file, threshold):
        # Calculate threshold value as a pixel number instead of percentage.
        width, height = Image.open(output_file).size
        threshold = int(width * height * threshold)

        diff_ppm = output_file.replace(".png", ".diff.ppm")
        cmd = "%s -threshold %d -output %s %s %s" % (
            self.perceptualdiff_path, threshold, diff_ppm, baseline_file, output_file)
        process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        perceptualdiff_stdout, _ = process.communicate()

        # Sometimes perceptualdiff returns a false positive with this exact message:
        # 'FAIL: Images are visibly different\n0 pixels are different\n\n'
        # We catch that here.
        if process.returncode == 0 or b'\n0 pixels are different' in perceptualdiff_stdout:
            # No differences found, but make sure to clean up the .ppm in case it was created.
            if os.path.exists(diff_ppm):
                os.remove(diff_ppm)
            return
        else:
            if os.path.exists(diff_ppm):
                if self.perceptualdiff_output_png:
                    # Convert the .ppm output to .png
                    diff_png = diff_ppm.replace("diff.ppm", "diff.png")
                    Image.open(diff_ppm).save(diff_png)
                    os.remove(diff_ppm)
                    diff_file_msg = ' (See %s)' % diff_png
                else:
                    diff_file_msg = ' (See %s)' % diff_ppm
            else:
                diff_file_msg = ''
            raise AssertionError("The new screenshot '%s' did not match "
                                 "the baseline '%s'%s:\n%s"
                                 % (output_file, baseline_file, diff_file_msg, perceptualdiff_stdout))


================================================
FILE: needle/engines/pil_engine.py
================================================
import sys
from itertools import chain
import math

if sys.version_info >= (3, 0):
    izip = zip
else:
    from itertools import izip

from PIL import Image

from needle.engines.base import EngineBase


class Engine(EngineBase):

    def assertSameFiles(self, output_file, baseline_file, threshold):
        output_image = Image.open(output_file).convert('RGB')
        baseline_image = Image.open(baseline_file).convert('RGB')
        diff = ImageDiff(output_image, baseline_image)
        distance = abs(diff.get_distance())
        if distance > threshold:
            raise AssertionError("The new screenshot '%s' did not match "
                                 "the baseline '%s' (by a distance of %.2f)"
                                 % (output_file, baseline_file, distance))


class ImageDiff(object):
    """
    Utility class for performing image comparisons using PIL.
    """

    def __init__(self, image_a, image_b):
        assert image_a.size == image_b.size
        assert image_a.getbands() == image_b.getbands()

        self.image_a = image_a
        self.image_b = image_b

    def get_nrmsd(self):
        """
        Returns the normalised root mean squared deviation of the two images.
        """
        a_values = chain(*self.image_a.getdata())
        b_values = chain(*self.image_b.getdata())
        rmsd = 0
        for a, b in izip(a_values, b_values):
            rmsd += (a - b) ** 2
        rmsd = math.sqrt(float(rmsd) / (
            self.image_a.size[0] * self.image_a.size[1] * len(self.image_a.getbands())
        ))
        return rmsd / 255

    def get_distance(self):
        """
        Returns the distance between the two images in pixels.
        """
        a_values = chain(*self.image_a.getdata())
        b_values = chain(*self.image_b.getdata())
        band_len = len(self.image_a.getbands())
        distance = 0
        for a, b in izip(a_values, b_values):
            distance += abs(float(a) / band_len - float(b) / band_len) / 255
        return distance

================================================
FILE: needle/js/__init__.py
================================================


================================================
FILE: needle/plugin.py
================================================
from nose.plugins import Plugin


class NeedleCapturePlugin(Plugin):
    """
    A nose plugin which causes all calls to
    ``NeedleTestCase.assertScreenshot`` to save a baseline screenshot to disk,
    unless the baseline file already exists.
    """
    name = 'needle-capture'

    def wantClass(self, cls):
        # Only gather classes which are a needle test case
        return hasattr(cls, 'assertScreenshot')

    def wantFunction(self, f):
        return False

    def beforeTest(self, test):
        if hasattr(test, 'test'):
            test.test.capture = True


class SaveBaselinePlugin(Plugin):
    """
    A nose plugin which causes all calls to ``NeedleTestCase.assertScreenshot``
    to save the baseline screenshot to disk.
    """
    name = 'save-baseline'

    def add_options(self, parser, env=None):
        super(SaveBaselinePlugin, self).add_options(parser, env)

    def wantClass(self, cls):
        # Only gather classes which are a needle test case
        return hasattr(cls, 'assertScreenshot')

    def wantFunction(self, f):
        return False

    def beforeTest(self, test):
        if hasattr(test, 'test'):
            test.test.save_baseline = True


class CleanUpOnSuccessPlugin(Plugin):
    """
    A nose plugin that causes all successful ``NeedleTestCase.assertScreenshot``
    calls to delete the non-baseline file from disk.
    """
    name = 'needle-cleanup-on-success'

    def add_options(self, parser, env=None):
        super(CleanUpOnSuccessPlugin, self).add_options(parser, env)

    def wantClass(self, cls):
        # Only gather classes which are a needle test case
        return hasattr(cls, 'assertScreenshot')

    def wantFunction(self, f):
        return False

    def beforeTest(self, test):
        if hasattr(test, 'test'):
            test.test.cleanup_on_success = True


================================================
FILE: setup.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
import sys
import os
import codecs

from needle import __version__

# Borrowed from
# https://github.com/jezdez/django_compressor/blob/develop/setup.py
def read(*parts):
    return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()


install_requires = [
    'nose>=1.0.0',
    'selenium>=2,<4',
    'pillow',
]

if sys.version_info < (2, 7, 0):
    # Install backport of unittest2 only if needed.
    install_requires.append('unittest2>=0.5.1')


setup(
    name='needle',
    version=__version__,
    description='Automated testing for your CSS.',
    author='Ben Firshman',
    author_email='ben@firshman.co.uk',
    url='https://github.com/python-needle/needle',
    packages=find_packages(exclude=['scripts', 'tests']),
    package_data={'needle': ['js/*']},
    test_suite='nose.collector',
    entry_points = {
        'nose.plugins.0.10': [
            'needle-capture = needle.plugin:NeedleCapturePlugin',
            'save-baseline = needle.plugin:SaveBaselinePlugin',
            'needle-cleanup-on-success = needle.plugin:CleanUpOnSuccessPlugin',
        ]
    },
    install_requires=install_requires,
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.2',
        'Programming Language :: Python :: 3.3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Topic :: Internet :: WWW/HTTP',
        'Topic :: Internet :: WWW/HTTP :: Browsers',
        'Topic :: Multimedia :: Graphics :: Capture :: Screen Capture',
        'Topic :: Software Development :: Libraries :: Python Modules',
        'Topic :: Software Development :: Testing',
    ],
)



================================================
FILE: tests/__init__.py
================================================
import sys

from PIL import Image, ImageDraw

if sys.version_info >= (3, 0):
    from io import BytesIO as IOClass
else:
    try:
        from cStringIO import StringIO as IOClass
    except ImportError:
        from StringIO import StringIO as IOClass


class ImageTestCaseMixin(object):

    def get_image(self, colour):
        return Image.new('RGB', (100, 100), colour)

    def get_black_image(self):
        return self.get_image((0, 0, 0))

    def get_white_image(self):
        return self.get_image((255, 255, 255))

    def get_half_filled_image(self):
        im = self.get_black_image()
        draw = ImageDraw.Draw(im)
        draw.rectangle(
            ((0, 0), (49, 100)),
            fill=(255, 255, 255)
        )
        return im

    def load_black_div(self, text=''):
        self.driver.load_html('''
            <style type="text/css">
                #black-box {
                    position: absolute;
                    left: 50px;
                    top: 100px;
                    width: 100px;
                    height: 100px;
                    background-color: black;
                    color: white;
                }
            </style>
            <div id="black-box">%s</div>
        ''' % text)

    def save_image_to_fh(self, im):
        fh = IOClass()
        im.save(fh, 'PNG')
        fh.seek(0)
        return fh

================================================
FILE: tests/plugin_test_cases/red_box.py
================================================
from needle.cases import NeedleTestCase
from tests import ImageTestCaseMixin

class RedBoxTestCase(ImageTestCaseMixin, NeedleTestCase):
    def test_red_box(self):
        self.driver.load_html("""
            <style type="text/css">
                #test {
                    position: absolute;
                    left: 50px;
                    top: 100px;
                    width: 100px;
                    height: 100px;
                    background-color: red;
                }
            </style>
            <div id="test"></div>
        """)
        self.assertScreenshot(self.driver.find_element_by_id('test'), 'red_box')

================================================
FILE: tests/test_diff.py
================================================
import math
import sys

if sys.version_info > (2, 7):
    from unittest import TestCase
else:
    from unittest2 import TestCase

from needle.engines.pil_engine import ImageDiff
from . import ImageTestCaseMixin


class TestImageDiff(ImageTestCaseMixin, TestCase):

    def test_nrmsd_all_channels(self):
        diff = ImageDiff(self.get_white_image(), self.get_black_image())
        self.assertEqual(diff.get_nrmsd(), 1)

    def test_nrmsd_one_channel(self):
        diff = ImageDiff(self.get_image((255, 0, 0)), self.get_black_image())
        self.assertEqual(diff.get_nrmsd(), math.sqrt(1.0 / 3))

    def test_nrmsd_half_filled(self):
        diff = ImageDiff(self.get_black_image(), self.get_half_filled_image())
        self.assertEqual(diff.get_nrmsd(), math.sqrt(0.5))

    def test_distance_all_channels(self):
        diff = ImageDiff(self.get_white_image(), self.get_black_image())
        self.assertAlmostEqual(diff.get_distance(), 100 * 100, delta=0.001)

    def test_distance_one_channel(self):
        diff = ImageDiff(self.get_image((255, 0, 0)), self.get_black_image())
        self.assertAlmostEqual(diff.get_distance(), 10000.0 / 3, delta=0.001)

    def test_distance_half_filled(self):
        diff = ImageDiff(self.get_black_image(), self.get_half_filled_image())
        self.assertAlmostEqual(diff.get_distance(), 10000.0 / 2, delta=0.001)





================================================
FILE: tests/test_driver.py
================================================
from needle.cases import NeedleTestCase

class TestWebDriver(NeedleTestCase):

    def test_load_html(self):
        self.driver.load_html('<div id="test">foo</div>')
        e = self.driver.find_element_by_id('test')
        self.assertEqual(e.text, 'foo')

    def test_load_html_works_with_large_pages(self):
        div = '<div>' + 'a' * 1000 + '</div>'
        html = ''.join(div for _ in range(500)) + '<div id="test">hello</div>'
        self.driver.load_html(html)
        self.assertEqual(
            self.driver.execute_script(
                'return document.getElementsByTagName("div").length'
            ),
            501
        )
        e = self.driver.find_element_by_id('test')
        self.assertEqual(e.text, 'hello')

    def test_load_jquery(self):
        self.driver.load_html('<div></div>')
        self.driver.load_jquery()
        self.assertTrue(self.driver.execute_script("""
            return jQuery !== undefined;
        """))


class TestWebElement(NeedleTestCase):

    def test_get_dimensions(self):
        self.driver.load_html("""
            <style type="text/css">
                #test {
                    position: absolute;
                    left: 50px;
                    top: 100px;
                    width: 150px;
                    height: 200px;
                }
            </style>
            <div id="test">Test</div>
        """)
        e = self.driver.find_element_by_id('test')
        self.assertEqual(e.get_dimensions(), {
            'left': 50,
            'top': 100,
            'width': 150,
            'height': 200,
        })

    def test_get_screenshot(self):
        self.driver.load_html("""
            <style type="text/css">
                #test {
                    position: absolute;
                    left: 50px;
                    top: 100px;
                    width: 150px;
                    height: 200px;
                    background-color: #FF0000;
                }
            </style>
            <div id="test"></div>
        """)
        e = self.driver.find_element_by_id('test')
        im = e.get_screenshot()
        self.assertEqual(im.size, (150, 200))
        for pixel in im.getdata():
            self.assertEqual(pixel, (255, 0, 0))


================================================
FILE: tests/test_imagemagick_engine.py
================================================
from __future__ import absolute_import
import subprocess
import unittest
from os import devnull

from tests.test_perceptualdiff_engine import PerceptualdiffEngineTests


class ImageMagickEngineTests(PerceptualdiffEngineTests):
    engine_class = 'needle.engines.imagemagick_engine.Engine'

    @classmethod
    def setUpClass(cls):
        try:
            subprocess.call(['compare', '--version'], stdout=open(devnull), stderr=open(devnull))
        except OSError:
            raise unittest.SkipTest('ImageMagick is not installed')
        super(ImageMagickEngineTests, cls).setUpClass()


# Delete the imported module so it doesn't get executed here.
del PerceptualdiffEngineTests

================================================
FILE: tests/test_in_memory.py
================================================
from __future__ import with_statement
from needle.cases import NeedleTestCase

from . import ImageTestCaseMixin


class InMemoryTests(ImageTestCaseMixin, NeedleTestCase):

    def create_div(self):
        self.driver.load_html("""
            <style type="text/css">
                #test {
                    position: absolute;
                    left: 50px;
                    top: 100px;
                    width: 100px;
                    height: 100px;
                    background-color: black;
                }
            </style>
            <div id="test"></div>
        """)

    def test_assertScreenshot(self):
        self.create_div()
        self.assertScreenshot(
            self.driver.find_element_by_id('test'),
            self.save_image_to_fh(self.get_black_image())
        )

    def test_assertScreenshot_with_css_selector(self):
        self.create_div()
        self.assertScreenshot(
            '#test',
            self.save_image_to_fh(self.get_black_image())
        )

    def test_assertScreenshot_fails(self):
        self.create_div()
        im = self.get_black_image()
        # Create one red pixel
        im.putpixel((0, 0), (255, 0, 0))
        with self.assertRaises(AssertionError):
            # Default threshold for error is 0
            self.assertScreenshot(
                self.driver.find_element_by_id('test'),
                self.save_image_to_fh(im)
            )

    def test_assertScreenshot_does_not_fail_with_threshold(self):
        self.create_div()
        im = self.get_black_image()
        # Create one red pixel
        im.putpixel((0, 0), (255, 0, 0))
        self.assertScreenshot(
            self.driver.find_element_by_id('test'),
            self.save_image_to_fh(im),
            threshold=1
        )

    def test_assertScreenshot_fails_with_threshold(self):
        self.create_div()
        im = self.get_black_image()
        # Create two white pixels
        im.putpixel((0, 0), (255, 255, 255))
        im.putpixel((1, 0), (255, 255, 255))
        with self.assertRaises(AssertionError):
            self.assertScreenshot(
                self.driver.find_element_by_id('test'),
                self.save_image_to_fh(im),
                threshold=1
            )

================================================
FILE: tests/test_perceptualdiff_engine.py
================================================
from __future__ import absolute_import
import subprocess
import unittest
from os import path, devnull

from tests.test_pil_engine import PILEngineTests


class PerceptualdiffEngineTests(PILEngineTests):
    engine_class = 'needle.engines.perceptualdiff_engine.Engine'

    @classmethod
    def setUpClass(cls):
        try:
            subprocess.call(['perceptualdiff', '--version'], stdout=open(devnull), stderr=open(devnull))
        except OSError:
            raise unittest.SkipTest('perceptualdiff is not installed')
        super(PerceptualdiffEngineTests, cls).setUpClass()

    def test_assertScreenshot_failure(self):
        super(PerceptualdiffEngineTests, self).test_assertScreenshot_failure()
        # Check that the diff file was created
        diff_file = path.join(path.dirname(__file__), 'screenshots', 'black-box.diff.png')
        self.assertTrue(path.isfile(diff_file))


# Delete the imported module so it doesn't get executed here.
del PILEngineTests

================================================
FILE: tests/test_pil_engine.py
================================================
from __future__ import absolute_import
import os
from os import path

from needle.cases import NeedleTestCase
from tests import ImageTestCaseMixin


class PILEngineTests(ImageTestCaseMixin, NeedleTestCase):
    engine_class = 'needle.engines.pil_engine.Engine'
    output_directory = 'tests/screenshots/'
    baseline_directory = 'tests/screenshots/baseline/'

    def setUp(self):
        # Remove all screeshots from previous test runs
        screenshots_path = path.join(path.dirname(__file__), 'screenshots')
        screenshots = [ f for f in os.listdir(screenshots_path) if f.endswith(".png") ]
        for screenshot in screenshots:
            os.remove(path.join(screenshots_path, screenshot))
        super(PILEngineTests, self).setUp()

    def test_assertScreenshot_success(self):
        self.load_black_div()
        self.assertScreenshot(
            self.driver.find_element_by_id('black-box'),
            'black-box'
        )

    def test_assertScreenshot_failure(self):
        with self.assertRaises(AssertionError):
            self.load_black_div('hello')
            self.assertScreenshot(
                self.driver.find_element_by_id('black-box'),
                'black-box'
            )

================================================
FILE: tests/test_plugin.py
================================================
import logging
import sys
import os
from errno import EEXIST

from needle.plugin import NeedleCapturePlugin, SaveBaselinePlugin, CleanUpOnSuccessPlugin
from nose.plugins import PluginTester

if sys.version_info > (2, 7):
    from unittest import TestCase
else:
    from unittest2 import TestCase


baseline_filename = 'screenshots/baseline/red_box.png'
screenshot_filename = 'screenshots/red_box.png'
dummy_baseline_content = b'abcd'
log = logging.getLogger(__name__)

def create_baseline_dir():
    """
    Create the baseline images directory if it doesn't already exist.
    """
    dir_path = 'screenshots/baseline'
    try:
        os.makedirs(dir_path)
    except OSError as err:
        if err.errno == EEXIST and os.path.isdir(dir_path):
            pass
        else:
            raise


class NeedlePluginTester(PluginTester):
    """
    Base class for tests of needle's nose plugins.
    """
    suitepath = 'tests/plugin_test_cases/red_box.py'

    def setUp(self):
        """
        Run the wrapped test suite and log its output for use in debugging
        failures.
        """
        super(NeedlePluginTester, self).setUp()
        log.debug(self.output)

    def tearDown(self):
        """
        Remove the baseline image created by the test.
        """
        os.remove(baseline_filename)


class NeedleCaptureTest(NeedlePluginTester, TestCase):
    """
    Check that the baseline file gets saved when using the
    --with-needle-capture option.
    """
    activate = '--with-needle-capture'
    plugins = [NeedleCapturePlugin()]

    def setUp(self):
        self.assertFalse(os.path.exists(baseline_filename))
        super(NeedleCaptureTest, self).setUp()

    def test_baseline_is_saved(self):
        self.assertTrue(os.path.exists(baseline_filename))
        self.assertTrue(self.nose.success)


class NeedleCaptureOverwriteTest(NeedlePluginTester, TestCase):
    """
    Check that an existing baseline file does NOT get overwritten, when using
    the --with-needle-capture option.
    """

    activate = '--with-needle-capture'
    plugins = [NeedleCapturePlugin()]

    def setUp(self):
        self.assertFalse(os.path.exists(baseline_filename))

        # Create dummy baseline file
        create_baseline_dir()
        baseline = open(baseline_filename, 'wb')
        baseline.write(dummy_baseline_content)
        baseline.close()

        super(NeedleCaptureOverwriteTest, self).setUp()

    def test_existing_baseline_not_overwritten(self):
        baseline = open(baseline_filename, 'rb')
        self.assertEqual(baseline.read(), dummy_baseline_content)
        self.assertTrue(self.nose.success)



class SaveBaselineTest(NeedlePluginTester, TestCase):
    """
    Check that the baseline file gets saved when using the
    --with-save-baseline option.
    """
    activate = '--with-save-baseline'
    plugins = [SaveBaselinePlugin()]

    def setUp(self):
        self.assertFalse(os.path.exists(baseline_filename))
        super(SaveBaselineTest, self).setUp()

    def test_baseline_is_saved(self):
        self.assertTrue(os.path.exists(baseline_filename))
        self.assertTrue(self.nose.success)


class SaveBaselineOverwriteTest(NeedlePluginTester, TestCase):
    """
    Check that an existing baseline file DOES get overwritten, when using
    the --with-save-baseline option.
    """

    activate = '--with-save-baseline'
    plugins = [SaveBaselinePlugin()]

    def setUp(self):
        self.assertFalse(os.path.exists(baseline_filename))

        # Create dummy baseline file
        create_baseline_dir()
        baseline = open(baseline_filename, 'wb')
        baseline.write(dummy_baseline_content)
        baseline.close()

        super(SaveBaselineOverwriteTest, self).setUp()

    def test_existing_baseline_is_overwritten(self):
        baseline = open(baseline_filename, 'rb')
        self.assertNotEqual(baseline.read(), dummy_baseline_content)
        self.assertTrue(self.nose.success)


class NeedleCleanupOnSuccessTest(NeedlePluginTester, TestCase):
    """
    Check that the screenshot gets removed when using the
    needle-cleanup-on-success option.
    """
    activate = '--with-needle-cleanup-on-success'
    plugins = [CleanUpOnSuccessPlugin()]

    def setUp(self):
        # Create the baseline
        create_baseline_dir()
        baseline = open(baseline_filename, 'wb')
        baseline.write(open('tests/test_red_box.png', 'rb').read())
        baseline.close()

        # Make sure the screenshot doesn't exist yet
        self.assertFalse(os.path.exists(screenshot_filename))
        super(NeedleCleanupOnSuccessTest, self).setUp()

    def test_screenshot_is_cleanedup(self):
        # Make sure the screenshot has been cleaned up
        self.assertFalse(os.path.exists(screenshot_filename))
        self.assertTrue(self.nose.success)


================================================
FILE: tox.ini
================================================
[tox]
envlist = py{27,34,35,36}-{chrome,firefox,phantomjs}

[testenv]
setenv =
    chrome: NEEDLE_BROWSER=chrome
    firefox: NEEDLE_BROWSER=firefox
    phantomjs: NEEDLE_BROWSER=phantomjs
commands = nosetests {posargs} -v

[testenv:docs]
deps = Sphinx==1.5.3
commands =
    sphinx-build -W -b {posargs:html} docs docs/_build
Download .txt
gitextract_ixilxv5m/

├── .gitignore
├── .travis.yml
├── CHANGES.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.txt
│   ├── make.bat
│   └── requirements.txt
├── needle/
│   ├── __init__.py
│   ├── cases.py
│   ├── driver.py
│   ├── engines/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── imagemagick_engine.py
│   │   ├── perceptualdiff_engine.py
│   │   └── pil_engine.py
│   ├── js/
│   │   └── __init__.py
│   └── plugin.py
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── plugin_test_cases/
│   │   └── red_box.py
│   ├── test_diff.py
│   ├── test_driver.py
│   ├── test_imagemagick_engine.py
│   ├── test_in_memory.py
│   ├── test_perceptualdiff_engine.py
│   ├── test_pil_engine.py
│   └── test_plugin.py
└── tox.ini
Download .txt
SYMBOL INDEX (113 symbols across 17 files)

FILE: needle/cases.py
  function _object_filename (line 32) | def _object_filename(obj):
  function import_from_string (line 36) | def import_from_string(path):
  class NeedleTestCase (line 46) | class NeedleTestCase(TestCase):
    method setUpClass (line 67) | def setUpClass(cls):
    method tearDownClass (line 85) | def tearDownClass(cls):
    method get_web_driver (line 94) | def get_web_driver(cls):
    method __init__ (line 125) | def __init__(self, *args, **kwargs):
    method set_viewport_size (line 151) | def set_viewport_size(cls, width, height):
    method assertScreenshot (line 161) | def assertScreenshot(self, element_or_selector, file, threshold=0):
    method compareScreenshot (line 173) | def compareScreenshot(self, element_or_selector, file, threshold=0):

FILE: needle/driver.py
  class NeedleWebElementMixin (line 38) | class NeedleWebElementMixin(object):
    method get_dimensions (line 45) | def get_dimensions(self):
    method get_screenshot (line 59) | def get_screenshot(self):
  class NeedleWebDriverMixin (line 90) | class NeedleWebDriverMixin(object):
    method load_html (line 94) | def load_html(self, html):
    method get_screenshot_as_image (line 101) | def get_screenshot_as_image(self):
    method load_jquery (line 109) | def load_jquery(self):
    method _get_js_path (line 119) | def _get_js_path(self):
    method create_web_element (line 122) | def create_web_element(self, element_id, *args, **kwargs):
  class NeedleRemote (line 129) | class NeedleRemote(NeedleWebDriverMixin, Remote):
  class NeedlePhantomJS (line 135) | class NeedlePhantomJS(NeedleWebDriverMixin, PhantomJS):
  class NeedleFirefox (line 141) | class NeedleFirefox(NeedleWebDriverMixin, Firefox):
  class NeedleChrome (line 147) | class NeedleChrome(NeedleWebDriverMixin, Chrome):
  class NeedleIe (line 153) | class NeedleIe(NeedleWebDriverMixin, Ie):
  class NeedleOpera (line 159) | class NeedleOpera(NeedleWebDriverMixin, Opera):
  class NeedleSafari (line 165) | class NeedleSafari(NeedleWebDriverMixin, Safari):
  class NeedleWebElement (line 171) | class NeedleWebElement(NeedleWebElementMixin, WebElement):
  class NeedleFirefoxWebElement (line 177) | class NeedleFirefoxWebElement(NeedleWebElementMixin, FirefoxWebElement):

FILE: needle/engines/base.py
  class EngineBase (line 1) | class EngineBase(object):
    method assertSameFiles (line 6) | def assertSameFiles(self, output_file, baseline_file, threshold):

FILE: needle/engines/imagemagick_engine.py
  class Engine (line 7) | class Engine(EngineBase):
    method assertSameFiles (line 12) | def assertSameFiles(self, output_file, baseline_file, threshold=0):

FILE: needle/engines/perceptualdiff_engine.py
  class Engine (line 9) | class Engine(EngineBase):
    method assertSameFiles (line 14) | def assertSameFiles(self, output_file, baseline_file, threshold):

FILE: needle/engines/pil_engine.py
  class Engine (line 15) | class Engine(EngineBase):
    method assertSameFiles (line 17) | def assertSameFiles(self, output_file, baseline_file, threshold):
  class ImageDiff (line 28) | class ImageDiff(object):
    method __init__ (line 33) | def __init__(self, image_a, image_b):
    method get_nrmsd (line 40) | def get_nrmsd(self):
    method get_distance (line 54) | def get_distance(self):

FILE: needle/plugin.py
  class NeedleCapturePlugin (line 4) | class NeedleCapturePlugin(Plugin):
    method wantClass (line 12) | def wantClass(self, cls):
    method wantFunction (line 16) | def wantFunction(self, f):
    method beforeTest (line 19) | def beforeTest(self, test):
  class SaveBaselinePlugin (line 24) | class SaveBaselinePlugin(Plugin):
    method add_options (line 31) | def add_options(self, parser, env=None):
    method wantClass (line 34) | def wantClass(self, cls):
    method wantFunction (line 38) | def wantFunction(self, f):
    method beforeTest (line 41) | def beforeTest(self, test):
  class CleanUpOnSuccessPlugin (line 46) | class CleanUpOnSuccessPlugin(Plugin):
    method add_options (line 53) | def add_options(self, parser, env=None):
    method wantClass (line 56) | def wantClass(self, cls):
    method wantFunction (line 60) | def wantFunction(self, f):
    method beforeTest (line 63) | def beforeTest(self, test):

FILE: setup.py
  function read (line 12) | def read(*parts):

FILE: tests/__init__.py
  class ImageTestCaseMixin (line 14) | class ImageTestCaseMixin(object):
    method get_image (line 16) | def get_image(self, colour):
    method get_black_image (line 19) | def get_black_image(self):
    method get_white_image (line 22) | def get_white_image(self):
    method get_half_filled_image (line 25) | def get_half_filled_image(self):
    method load_black_div (line 34) | def load_black_div(self, text=''):
    method save_image_to_fh (line 50) | def save_image_to_fh(self, im):

FILE: tests/plugin_test_cases/red_box.py
  class RedBoxTestCase (line 4) | class RedBoxTestCase(ImageTestCaseMixin, NeedleTestCase):
    method test_red_box (line 5) | def test_red_box(self):

FILE: tests/test_diff.py
  class TestImageDiff (line 13) | class TestImageDiff(ImageTestCaseMixin, TestCase):
    method test_nrmsd_all_channels (line 15) | def test_nrmsd_all_channels(self):
    method test_nrmsd_one_channel (line 19) | def test_nrmsd_one_channel(self):
    method test_nrmsd_half_filled (line 23) | def test_nrmsd_half_filled(self):
    method test_distance_all_channels (line 27) | def test_distance_all_channels(self):
    method test_distance_one_channel (line 31) | def test_distance_one_channel(self):
    method test_distance_half_filled (line 35) | def test_distance_half_filled(self):

FILE: tests/test_driver.py
  class TestWebDriver (line 3) | class TestWebDriver(NeedleTestCase):
    method test_load_html (line 5) | def test_load_html(self):
    method test_load_html_works_with_large_pages (line 10) | def test_load_html_works_with_large_pages(self):
    method test_load_jquery (line 23) | def test_load_jquery(self):
  class TestWebElement (line 31) | class TestWebElement(NeedleTestCase):
    method test_get_dimensions (line 33) | def test_get_dimensions(self):
    method test_get_screenshot (line 54) | def test_get_screenshot(self):

FILE: tests/test_imagemagick_engine.py
  class ImageMagickEngineTests (line 9) | class ImageMagickEngineTests(PerceptualdiffEngineTests):
    method setUpClass (line 13) | def setUpClass(cls):

FILE: tests/test_in_memory.py
  class InMemoryTests (line 7) | class InMemoryTests(ImageTestCaseMixin, NeedleTestCase):
    method create_div (line 9) | def create_div(self):
    method test_assertScreenshot (line 24) | def test_assertScreenshot(self):
    method test_assertScreenshot_with_css_selector (line 31) | def test_assertScreenshot_with_css_selector(self):
    method test_assertScreenshot_fails (line 38) | def test_assertScreenshot_fails(self):
    method test_assertScreenshot_does_not_fail_with_threshold (line 50) | def test_assertScreenshot_does_not_fail_with_threshold(self):
    method test_assertScreenshot_fails_with_threshold (line 61) | def test_assertScreenshot_fails_with_threshold(self):

FILE: tests/test_perceptualdiff_engine.py
  class PerceptualdiffEngineTests (line 9) | class PerceptualdiffEngineTests(PILEngineTests):
    method setUpClass (line 13) | def setUpClass(cls):
    method test_assertScreenshot_failure (line 20) | def test_assertScreenshot_failure(self):

FILE: tests/test_pil_engine.py
  class PILEngineTests (line 9) | class PILEngineTests(ImageTestCaseMixin, NeedleTestCase):
    method setUp (line 14) | def setUp(self):
    method test_assertScreenshot_success (line 22) | def test_assertScreenshot_success(self):
    method test_assertScreenshot_failure (line 29) | def test_assertScreenshot_failure(self):

FILE: tests/test_plugin.py
  function create_baseline_dir (line 20) | def create_baseline_dir():
  class NeedlePluginTester (line 34) | class NeedlePluginTester(PluginTester):
    method setUp (line 40) | def setUp(self):
    method tearDown (line 48) | def tearDown(self):
  class NeedleCaptureTest (line 55) | class NeedleCaptureTest(NeedlePluginTester, TestCase):
    method setUp (line 63) | def setUp(self):
    method test_baseline_is_saved (line 67) | def test_baseline_is_saved(self):
  class NeedleCaptureOverwriteTest (line 72) | class NeedleCaptureOverwriteTest(NeedlePluginTester, TestCase):
    method setUp (line 81) | def setUp(self):
    method test_existing_baseline_not_overwritten (line 92) | def test_existing_baseline_not_overwritten(self):
  class SaveBaselineTest (line 99) | class SaveBaselineTest(NeedlePluginTester, TestCase):
    method setUp (line 107) | def setUp(self):
    method test_baseline_is_saved (line 111) | def test_baseline_is_saved(self):
  class SaveBaselineOverwriteTest (line 116) | class SaveBaselineOverwriteTest(NeedlePluginTester, TestCase):
    method setUp (line 125) | def setUp(self):
    method test_existing_baseline_is_overwritten (line 136) | def test_existing_baseline_is_overwritten(self):
  class NeedleCleanupOnSuccessTest (line 142) | class NeedleCleanupOnSuccessTest(NeedlePluginTester, TestCase):
    method setUp (line 150) | def setUp(self):
    method test_screenshot_is_cleanedup (line 161) | def test_screenshot_is_cleanedup(self):
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (75K chars).
[
  {
    "path": ".gitignore",
    "chars": 194,
    "preview": "*.pyc\n*.egg-info\n.DS_Store\n*~\n.*.swo\n.*.swp\n.*.swn\n.*.swm\n/.tox/\n/build/\n/dist/\nselenium-server-standalone-*.jar\n/docs/_"
  },
  {
    "path": ".travis.yml",
    "chars": 1648,
    "preview": "language: python\nsudo: required\ndist: trusty\npython:\n  - \"2.7\"\n  - \"3.2\"\n  - \"3.3\"\n  - \"3.4\"\n  - \"3.5\"\n  - \"3.6\"\n# Firef"
  },
  {
    "path": "CHANGES.md",
    "chars": 2650,
    "preview": "Change log\n==========\n\nUpcoming 0.5.0\n--------------\n\n- Dropped Python 2.6 support.\n- Verified support for Python 3.2-3."
  },
  {
    "path": "LICENSE",
    "chars": 1469,
    "preview": "Copyright (c) 2011, Ben Firshman\nAll rights reserved.\n \nRedistribution and use in source and binary forms, with or witho"
  },
  {
    "path": "MANIFEST.in",
    "chars": 59,
    "preview": "include LICENSE\ninclude *.md\nrecursive-include needle/js *\n"
  },
  {
    "path": "README.md",
    "chars": 1365,
    "preview": "Needle\n======\n\n[![Build Status](https://travis-ci.org/python-needle/needle.png?branch=master)](https://travis-ci.org/pyt"
  },
  {
    "path": "docs/Makefile",
    "chars": 4590,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/conf.py",
    "chars": 6980,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Needle documentation build configuration file, created by\n# sphinx-quickstart on Tue Apr  5 "
  },
  {
    "path": "docs/index.txt",
    "chars": 5509,
    "preview": "Needle: Automated tests for your visuals\n========================================\n\nNeedle is a tool for testing your CSS"
  },
  {
    "path": "docs/make.bat",
    "chars": 4511,
    "preview": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUI"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 61,
    "preview": "# Used by ReadTheDocs for documentation builds\nSphinx==1.5.3\n"
  },
  {
    "path": "needle/__init__.py",
    "chars": 143,
    "preview": "__version__ = '0.5.0'\n__author__ = 'Ben Firshman'\n__contact__ = 'ben@firshman.co.uk'\n__homepage__ = 'https://github.com/"
  },
  {
    "path": "needle/cases.py",
    "chars": 9772,
    "preview": "# encoding: utf-8\nfrom __future__ import absolute_import\nfrom __future__ import print_function\n\nfrom warnings import war"
  },
  {
    "path": "needle/driver.py",
    "chars": 5938,
    "preview": "# encoding: utf-8\nfrom __future__ import absolute_import\n\nimport base64\nimport os\nimport sys\n\nif sys.version_info >= (3,"
  },
  {
    "path": "needle/engines/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "needle/engines/base.py",
    "chars": 179,
    "preview": "class EngineBase(object):\n    \"\"\"\n    Base class for diff engines.\n    \"\"\"\n\n    def assertSameFiles(self, output_file, b"
  },
  {
    "path": "needle/engines/imagemagick_engine.py",
    "chars": 1465,
    "preview": "import os\nimport subprocess\n\nfrom needle.engines.base import EngineBase\n\n\nclass Engine(EngineBase):\n    compare_path = \""
  },
  {
    "path": "needle/engines/perceptualdiff_engine.py",
    "chars": 2048,
    "preview": "import subprocess\nimport os\n\nfrom PIL import Image\n\nfrom needle.engines.base import EngineBase\n\n\nclass Engine(EngineBase"
  },
  {
    "path": "needle/engines/pil_engine.py",
    "chars": 2018,
    "preview": "import sys\nfrom itertools import chain\nimport math\n\nif sys.version_info >= (3, 0):\n    izip = zip\nelse:\n    from itertoo"
  },
  {
    "path": "needle/js/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "needle/plugin.py",
    "chars": 1842,
    "preview": "from nose.plugins import Plugin\n\n\nclass NeedleCapturePlugin(Plugin):\n    \"\"\"\n    A nose plugin which causes all calls to"
  },
  {
    "path": "setup.py",
    "chars": 2206,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\nfrom setuptools import setup, find_packages\nimport sys\nimport os\nimport co"
  },
  {
    "path": "tests/__init__.py",
    "chars": 1367,
    "preview": "import sys\n\nfrom PIL import Image, ImageDraw\n\nif sys.version_info >= (3, 0):\n    from io import BytesIO as IOClass\nelse:"
  },
  {
    "path": "tests/plugin_test_cases/red_box.py",
    "chars": 640,
    "preview": "from needle.cases import NeedleTestCase\nfrom tests import ImageTestCaseMixin\n\nclass RedBoxTestCase(ImageTestCaseMixin, N"
  },
  {
    "path": "tests/test_diff.py",
    "chars": 1372,
    "preview": "import math\nimport sys\n\nif sys.version_info > (2, 7):\n    from unittest import TestCase\nelse:\n    from unittest2 import "
  },
  {
    "path": "tests/test_driver.py",
    "chars": 2256,
    "preview": "from needle.cases import NeedleTestCase\n\nclass TestWebDriver(NeedleTestCase):\n\n    def test_load_html(self):\n        sel"
  },
  {
    "path": "tests/test_imagemagick_engine.py",
    "chars": 684,
    "preview": "from __future__ import absolute_import\nimport subprocess\nimport unittest\nfrom os import devnull\n\nfrom tests.test_percept"
  },
  {
    "path": "tests/test_in_memory.py",
    "chars": 2258,
    "preview": "from __future__ import with_statement\nfrom needle.cases import NeedleTestCase\n\nfrom . import ImageTestCaseMixin\n\n\nclass "
  },
  {
    "path": "tests/test_perceptualdiff_engine.py",
    "chars": 976,
    "preview": "from __future__ import absolute_import\nimport subprocess\nimport unittest\nfrom os import path, devnull\n\nfrom tests.test_p"
  },
  {
    "path": "tests/test_pil_engine.py",
    "chars": 1218,
    "preview": "from __future__ import absolute_import\nimport os\nfrom os import path\n\nfrom needle.cases import NeedleTestCase\nfrom tests"
  },
  {
    "path": "tests/test_plugin.py",
    "chars": 4835,
    "preview": "import logging\nimport sys\nimport os\nfrom errno import EEXIST\n\nfrom needle.plugin import NeedleCapturePlugin, SaveBaselin"
  },
  {
    "path": "tox.ini",
    "chars": 326,
    "preview": "[tox]\nenvlist = py{27,34,35,36}-{chrome,firefox,phantomjs}\n\n[testenv]\nsetenv =\n    chrome: NEEDLE_BROWSER=chrome\n    fir"
  }
]

About this extraction

This page contains the full source code of the bfirsh/needle GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (68.9 KB), approximately 17.9k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!