Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dbaty committed Jun 1, 2020
0 parents commit 00d1874
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testfinder
29 changes: 29 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Copyright (c) 2020, Damien Baty
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.

* Neither the name of testfinder nor the names of its contributors may
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 POLYCONSEIL 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.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# FIXME: add option to strip debug symbols
testfinder: testfinder.go
go build -ldflags '-s' testfinder.go

.PHONY: build
build: testfinder

.PHONY: test
test:
go test
88 changes: 88 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
``testfinder`` outputs a list of Python test cases and test functions
found from the requested directory.

I feed ``testfinder`` output to `fzf`_ to build auto-completion for a
test runner:

.. image:: https://raw.githubusercontent.com/dbaty/testfinder/master/docs/demo.svg
:width: 100%

The demo above is me typing ``pytest`` followed by ``<Maj>-<Tab>``
(which is my configured key for "advanced" auto-completion) and then
typing characters of the test function I am looking for.

``testfinder`` is fast enough for me: the first list of unfiltered
suggestions appear almost instantaneously. Then ``fzf`` does its
magic, in an even more instantaneous fashion.

On a Python project with 477 test files amongst 995 files in the tests
directory, with almost 5000 test cases and functions, ``testfinder``
takes 10ms. If it's slower for you, you're eligible for a refund.

The latest binary is at `<https://github.com/dbaty/testfinder/releases>`_.


.. _fzf: https://github.com/junegunn/fzf


Example
=======

.. code:: bash
$ testfinder
tests/tests.py::TestClassWithMethods::test_method1
tests/tests.py::TestClassWithMethods::test_method2
tests/tests.py::test_func
Command-line options:

FIXME: make it configurable: starting directory; filename patterns


Installation
============

FIXME

The latest version for linux/amd64 can be found at `https://github.com/dbaty/testfinder/releases`_.
It has been built with ``make build``.

Alternatively, you may build the sources yourself:

.. code: bash
$ go get https://github.com/dbaty/testfinder
$ $GOPATH/bin/testfinder -v
0.1
Usage for auto-completion
=========================

To use with ``fzf`` on ``pytest``, add this in ``.zshrc`` (or adapt
for your shell)::

.. console::

_fzf_complete_pytest() {
_fzf_complete "--multi --reverse" "$@" < <(testfinder)
}


Status, limitations, future
===========================

It's tailored and works for Python code only for now. The output is
compatible with ``pytest``. File parsing is very simple ("fragile" is
another word that comes to mind), and yet it works surprisingly well
in standard cases.

It's my first program in Go. I skipped "Hello world". Maybe I should
not have. If it looks too much like a Python programmer struggling to
write Go, feel free to educate me. Pull requests are welcome.

Future plans:

- handle other programming languages (not planned yet);
- pivot, disrupt an industry and take over the world (ditto).
68 changes: 68 additions & 0 deletions docs/demo.cast

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions testdata/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class TestEmptyClass:
pass


class TestClassWithMethods:

def test_method1(self):
pass

def test_method2(self):
def test_inner_function_to_ignore():
pass


def test_func():
pass
90 changes: 90 additions & 0 deletions testfinder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Print a list of test cases and test functions found from the requested
directory (or from the "tests/" directory by default).
This can be used for auto-completion.
*/

package main

import "bufio"
import "fmt"
import "os"
import "path/filepath"
import "regexp"
import "strings"


var pythonRegexpClassName = regexp.MustCompile("^class (\\w*)")
var pythonRegexpFuncName = regexp.MustCompile("^( )?def (test\\w*)")


func parsePythonFile(path string) []string {
module := strings.TrimSuffix(path, ".py")
module = strings.Replace(module, "/", ".", -1)
functions := make([]string, 0)
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
currentClass := ""
for scanner.Scan() {
line := scanner.Text()
match := pythonRegexpClassName.FindStringSubmatch(line)
if len(match) > 0 {
currentClass = match[1]
functions = append(functions, path + "::" + currentClass)
} else {
match := pythonRegexpFuncName.FindStringSubmatch(line)
if len(match) > 0 {
var funcName = path + "::" + match[2]
if match[1] != "" {
funcName = path + "::" + currentClass + "::" + match[2]
}
functions = append(functions, funcName)
}
}
}
return functions
}


func walkDir(dir string, filePattern string) ([]string, error) {
var testFunctions = make([]string, 0)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
isTestFile, _ := regexp.MatchString(filePattern, path)
if isTestFile {
testFunctions = append(testFunctions, parsePythonFile(path)...)
}
}
return nil // no error
})
return testFunctions, err
}


func main() {
startDir := ""
if len(os.Args) < 2 {
startDir = "tests"
} else {
startDir = os.Args[1]
}
filePattern := "(tests\\.py$)|(test_.*?\\.py$)"
var testFunctions, err = walkDir(startDir, filePattern)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, testFunc := range testFunctions {
fmt.Println(testFunc)
}
}
20 changes: 20 additions & 0 deletions testfinder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import "reflect"
import "testing"


func TestParsePythonFile(t *testing.T) {
functions := parsePythonFile("testdata/test.py")
expected := []string{
"testdata/test.py::TestEmptyClass",
"testdata/test.py::TestClassWithMethods",
"testdata/test.py::TestClassWithMethods::test_method1",
"testdata/test.py::TestClassWithMethods::test_method2",
"testdata/test.py::test_func",
}

if !reflect.DeepEqual(functions, expected) {
t.Errorf("got %s, expected %s.", functions, expected)
}
}

0 comments on commit 00d1874

Please sign in to comment.