Skip to content

Commit 816cd4f

Browse files
committed
Introducing the -watch option, and waaaay more!
So this commit is massive. Sorry about that. I got carried away adding -watch support and ended up improving a lot of other things along the way. Current qa -watch features include runtime dependency analysis of any require, load, and require_relative. Editing a test file will re-run all test cases in that file, but editing a dependency of a single test will only re-run test that use that functionality. Future work may introduce finer- grained logic. Debut (and rename) "qa auto" as "qa watch". This functionality is available on any qa test run command. For example: qa minitest -watch Globs used by the -watch option support {foo,bar} style expressions (in addition to * and **), so you can do obscene things like: qa test-unit -watch 'test/{unit,functional}/**/test*.rb' Note the single quotes so the shell doesn't expand the pattern and prevent the filesystem watcher from discovering new tests that fit the pattern. Split -warmup into -warmup and -eager-load. This way we can avoid -eager-load when capturing runtime dependencies, which would introduce noise. Make -warmup default to true again, so basic rails tests can pass out of the box with a -jobs value > 1. Make test filter for rspec deterministic across runs (other frameworks already had this property). This was required for -watch functionality. Add -memprofile and -heapdump command line options to help diagnose memory usage problems within qa itself. This was important for getting heap usage low enough for -watch to be useful on large rails projects like discourse. Improve support for syntax and load errors during test runs. This was also important for -watch to be useful in the real word. Also add automated demo asciicast creation. A decent amount of work in this behemoth of a change was thanks to iterating on qa so the demo asciicasts would look reasonable. For example: - Strip minitest paths from backtraces. - Refine normal qa run output to be tighter and easier to scan. - Tweak colors and ordering of snail report. - Fix test seeding to actually generate different values each qa run, discovered while trying to generate interesting data for a `qa flaky` demo. - `qa flaky` output has color and no more cruft. - `qa flaky` shows error messages in overview. - `qa flaky` shows relative frequency of each type of error. Other changes that got sucked into this change and couldn't escape its gravity well: - Rename tapjio event types to have more regular, sane names. - Fix a bug in minitest spec support that caused problems for tests that had different but identically named describe blocks. Thanks to @halostatue for reporting it! - Add framework-specific commands, so tab completion works. So instead of: qa run minitest:test/**/test*.rb use qa minitest test/**/test*.rb - Fix hang after ctrl-c of qa run command. - Enable local variable capture by default on macOS, leave disabled by default on Linux (for stability reasons). Finally, update README.md and include the most basic asciicast.
1 parent b8411a3 commit 816cd4f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4448
-813
lines changed

Gimmefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"commands": [
1919
"env GOPATH=$PWD GOGENPATH=$GIMME_SCRATCH/src go generate -v -x qa/main qa/runner qa/tapjio qa/analysis",
2020
"env GOPATH=$PWD:$GIMME_SCRATCH go build -o $GIMME_OUTPUT/bin/qa qa/main",
21-
"env GOPATH=$PWD:$GIMME_SCRATCH go test -v -race qa_test",
21+
"env GOPATH=$PWD:$GIMME_SCRATCH go test -v -race $(env GOPATH=$PWD go list ./... | grep -v /vendor/)",
2222
"true"
2323
],
2424
"prepend-platform-env": {

README.md

+23-18
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,39 @@
22

33
QA is a lightweight tool for running your tests *fast*.
44

5-
For years, the software testing ecosystem has lagged behind other parts of the software development pipeline. Advances in type systems, compiler technology, and prototyping environments (to name a few) have helped make software engineers much more productive. QA is an effort to make similar strides for automated testing tools.
5+
[![qa minitest asciicast](https://asciinema.org/a/d94fig03bbkmnzdy4mtk87ns4.png)](https://asciinema.org/a/d94fig03bbkmnzdy4mtk87ns4)
6+
7+
Advances in type systems, compiler technology, and prototyping environments (to name a few) have helped make many software engineering activities more productive. QA is an effort to make similar strides for automated testing tools.
68

79
## What can QA help me do today?
810

9-
1. Run your tests faster. Run `qa run <type>` in your project directory and watch your test results scream by as they run in parallel. QA provides a beautiful, easy to understand report. No Rakefile necessary!
11+
1. Run your tests faster. Run `qa rspec`, `qa minitest`, or `qa test-unit` in your project directory and watch your test results scream by as they run in parallel. QA provides a beautiful, easy to understand report. No Rakefile necessary!
1012

11-
2. See which tests are slowing down your testrun. QA highlights tests that are dramatically slower than the average test duration. Look for the 🐌 at the end of successful testrun!
13+
2. See which tests are slowing you down. QA highlights tests that are dramatically slower than average. Look for the 🐌 at the end of successful testrun!
1214

1315
3. See per-test stderr and stdout. Even when running tests in parallel!
1416

1517
4. Investigate test performance by generating flamegraphs (or icicle graph) for the entire testrun. See `-save-flamegraph`, `-save-icegraph`, and `-save-palette`.
1618

17-
5. Run your tests in parallel. QA does this for you automatically, for test types `rspec`, `rspec-pendantic`, `minitest`, `minitest-pendantic`, `test-unit`, and `test-unit-pendantic`. The `-pendantic` suffix runs each *method* in a separate child process. (Versus each *case* in its own worker process.)
19+
5. Run your tests in parallel. QA does this for you automatically. Use `-squash=none` to run each *method* in a separate child process. The default is `-squash=file`, which runs each *file* in its own process.
1820

19-
6. Analyze and eliminate [test flakiness](#whatis_flaky). The `-archive-base-dir` option for `qa run` records test outcomes across different runs. Use the `qa flaky` command with the same `-archive-base-dir` option to identify and diagnose flaky tests. This is new functionality, so please [open an issue](https://github.com/ajbouh/qa/issues/new) with questions and feedback!
21+
6. Analyze and eliminate [test flakiness](#whatis_flaky). The `-archive-base-dir` option records test outcomes across different runs. Use the `qa flaky` command with the same `-archive-base-dir` option to identify and diagnose flaky tests. This is new functionality, so please [open an issue](https://github.com/ajbouh/qa/issues/new) with questions and feedback!
2022

2123
7. Track threads, GC, require, SQL queries, and other noteworthy operations in a tracing format that can be used with the `chrome://tracing` tool, using `-save-trace` option.
2224

23-
8. See source code snippets and (with the experimental `-errors-capture-locals`) actual values of local variables for each from of an error's stack trace.
25+
8. See source code snippets and actual values of local variables for each frame of an error's stack trace.
2426

2527
9. Record test output as TAP-J, using `-save-tapj` option.
2628

2729
10. Automatically partition Rails tests across multiple databases, one per worker (Using custom ActiveRecord integration logic). If the required test databases do not exist, they will be setup automatically before tests begin. NOTE This functionality is highly experimental. Disable it with `-warmup=false`. Please [open an issue](https://github.com/ajbouh/qa/issues/new) if you have trouble.
2830

2931
## What languages and test frameworks does QA support?
3032

31-
Ruby's RSpec, MiniTest, test-unit. Be sure to use `bundle exec` when you run qa, if you're managing dependencies with Bundler. For example, if you're using rspec:
33+
Ruby 2.3+, and any of: RSpec, MiniTest, test-unit.
34+
35+
Be sure to use `bundle exec` when you run qa, if you're managing dependencies with Bundler. For example, if you're using Rspec:
3236
```
33-
bundle exec qa run rspec
37+
bundle exec qa rspec
3438
```
3539

3640
## What will QA help me do tomorrow?
@@ -66,15 +70,15 @@ test/
6670
Example usage and output:
6771
```
6872
> cd $project
69-
> qa run minitest
73+
> qa minitest
7074
...
7175
```
7276

7377
## Troubleshooting QA
7478

7579
Since QA is still in alpha, there are a number of rough edges.
7680

77-
If `qa run` seems to be acting strangely and isn't providing a reasonable error message, you may be experiencing a bug relating to swallowed error output. This is tied to QA's stdout and stderr capture logic. Adding the `-capture-standard-fds=false` option will disable the capture logic and should allow the original error to bubble up. Please [open an issue](https://github.com/ajbouh/qa/issues/new) with the error output.
81+
If `qa` seems to be acting strangely and isn't providing a reasonable error message, you may be experiencing a bug relating to swallowed error output. This is tied to QA's stdout and stderr capture logic. Adding the `-capture-standard-fds=false` option will disable the capture logic and should allow the original error to bubble up. Please [open an issue](https://github.com/ajbouh/qa/issues/new) with the error output.
7882

7983
## What are flaky tests?<a name="whatis_flaky"></a>
8084

@@ -87,21 +91,21 @@ So that's the bad news: by their very nature, flaky tests are hard to avoid. In
8791
## How do I use QA to detect flaky tests?
8892
An example session
8993
```
90-
$ qa run -archive-base-dir ~/.qa/archive
94+
$ qa minitest -archive-base-dir ~/.qa/archive
9195
# ... unexpected test failure
92-
$ qa run -archive-base-dir ~/.qa/archive
96+
$ qa minitest -archive-base-dir ~/.qa/archive
9397
# ... that same test now passes
9498
```
9599

96-
To analyze the last few days worth of test results, you can use the `qa flaky` command. It's important to use the same value for `-archive-base-dir` as given to `qa run`. For example, continuing the session from above:
100+
To analyze the last few days worth of test results, you can use the `qa flaky` command. It's important to use the same value for `-archive-base-dir` as given to other `qa` commands. For example, continuing the session from above:
97101

98102
```
99103
$ qa flaky -archive-base-dir ~/.qa/archive
100104
```
101105

102106
## How does QA detect flaky tests?
103107

104-
At a high level, QA considers a test to be flaky if, for a particular code revision, that test has both passed and failed. That's why you should provide a `-suite-coderef` value to `qa run`.
108+
At a high level, QA considers a test to be flaky if, for a particular code revision, that test has both passed and failed. That's why you should provide a `-suite-coderef` value to `qa` commands.
105109

106110
At a low level, QA uses a few tricks to find as many examples of a flaky failure as it can. The actual algorithm for discovering flaky tests is:
107111
- Fingerprint all failures using:
@@ -111,8 +115,8 @@ At a low level, QA uses a few tricks to find as many examples of a flaky failure
111115
- Find all tests that, for a single revision, have both passed and failed.
112116
- Put test failures from different revisions in the same bucket if their fingerprint matches a known flaky test
113117

114-
## How will QA help me with test flakiness?
115-
Now the good news: with QA, we've set out to address the shortcomings we see with today's testing tools. We want a toolset that's *fast* and gives us more firepower for dealing with the reality of flaky tests.
118+
## How will future versions of QA help me with test flakiness?
119+
With QA, we've set out to address the shortcomings we see with today's testing tools. We want a toolset that's *fast* and gives us more firepower for dealing with the reality of flaky tests.
116120

117121
- **Testing code that includes dependencies you didn't write?** QA will isolate tests from network services using an OS-specific sandbox.
118122

@@ -153,7 +157,7 @@ Now the good news: with QA, we've set out to address the shortcomings we see wit
153157
- [X] Add TAP-J analysis tools, to detect rates of flakiness in tests
154158
- [ ] Add support for marking some tests as (implicitly?) new, forcing them to be run many times and pass every time
155159
- [ ] Add support for marking tests as flaky, separating their results from the results of other tests
156-
- [ ] For tests that are failing flakily, show distribution of which line failed, test duration, version of code
160+
- [x] For tests that are failing flakily, show distribution of which line failed, test duration, version of code
157161

158162
### Continuous integration
159163
- [ ] Add support for auto-filing issues (or updating existing issues) when a merged test fails that should not be flaky
@@ -162,7 +166,8 @@ Now the good news: with QA, we've set out to address the shortcomings we see wit
162166
### Local development
163167
- [ ] Order test run during local development based on what's failed recently
164168
- [ ] Line-level code coverage report
165-
- [ ] Rerunning tests during local development affected by what code you just modified (test code or AUT, using code coverage analysis)
169+
- [x] Rerunning tests during local development affected by what code you just modified (test code or AUT, using code coverage analysis)
170+
- [ ] Line-level test rerunning, using code coverage
166171
- [ ] Limit tests to files that are open in editor (open test files, open AUT files, etc)
167172
- [ ] Can run with git-bisect to search for commit that introduced a bug
168173
- [ ] Suggest which failing tests to debug first (based on heuristics)

demos/flaky/flaky.script

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# qa flaky
2+
3+
# We'll be using the ruby-mime-types test suite to
4+
# demonstrate finding flaky tests with qa.
5+
#
6+
git remote get-url origin; git reset --hard aa499d1; tree ./test
7+
8+
#
9+
# First fix a parallelism bug in the cache tests.
10+
# Test processes must use separate scratch files.
11+
#
12+
git apply ../patches/fix-cache-test-parallelism.patch
13+
git diff -U2; git add --update
14+
15+
#
16+
# Break one test so it will never pass, and another
17+
# test to pass ~25% of the time, fail one way ~50%
18+
# of the time, and otherwise fail another way.
19+
#
20+
git apply ../patches/introduce-faults.patch
21+
git diff -U2; git add --update
22+
23+
#
24+
# Record enough data for qa to analyze.
25+
#
26+
for x in $(seq 12)
27+
do
28+
bundle exec qa run -archive-base-dir=.qa-archive -quiet minitest
29+
done
30+
31+
#
32+
# Now use qa to find that flaky test!
33+
#
34+
qa flaky -archive-base-dir=.qa-archive
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff --git a/test/test_mime_types_cache.rb b/test/test_mime_types_cache.rb
2+
index 3b5859b..dd359d6 100644
3+
--- a/test/test_mime_types_cache.rb
4+
+++ b/test/test_mime_types_cache.rb
5+
@@ -12,7 +12,7 @@ describe MIME::Types::Cache do
6+
require 'fileutils'
7+
8+
MUTEX.synchronize do
9+
- @cache_file = File.expand_path('../cache.tst', __FILE__)
10+
+ @cache_file = File.expand_path("../cache.tst#{ENV['QA_WORKER']}", __FILE__)
11+
ENV['RUBY_MIME_TYPES_CACHE'] = @cache_file
12+
clear_cache_file
13+
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
diff --git a/test/test_mime_types.rb b/test/test_mime_types.rb
2+
index caadc37..1ded080 100644
3+
--- a/test/test_mime_types.rb
4+
+++ b/test/test_mime_types.rb
5+
@@ -117,9 +117,9 @@ describe MIME::Types do
6+
end
7+
8+
it 'successfully adds from another MIME::Types' do
9+
- mt = MIME::Types.new
10+
+ mt = rand(2) == 0 ? nil : MIME::Types.new
11+
mt.add(mime_types)
12+
- assert_equal mime_types.count, mt.count
13+
+ assert_equal mime_types.count, rand(2) == 0 ? mt.count : -1
14+
15+
mime_types.each do |type|
16+
assert_equal mt[type.content_type], [ type ]
17+
@@ -155,7 +155,7 @@ describe MIME::Types do
18+
19+
describe '#count' do
20+
it 'can count the number of types inside' do
21+
- assert_equal 6, mime_types.count
22+
+ assert_equal 4, mime_types.count
23+
end
24+
end
25+
end

demos/record-2script.sh

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/bin/bash
2+
3+
# usage:
4+
# record-2script.sh <script_file> <output_path>
5+
set -e
6+
7+
SCRIPT=$1
8+
OUTPUT_PATH=$2
9+
10+
# Title is first line of script, with leading "# " removed.
11+
TITLE=$(head -n 1 $SCRIPT | tail -c +3)
12+
DEMO_SEMAPHORE=$PWD/.tmux-semaphore
13+
DEMO_RCFILE=$PWD/.bashrc
14+
MAX_WAIT=2
15+
HEIGHT=30
16+
WIDTH=200
17+
COMMENT_KEY_DELAY=0.02
18+
COMMENT_SPACE_DELAY=0.18
19+
COMMAND_KEY_DELAY=0.06
20+
LINE_DELAY=1.8
21+
22+
trap '(test -e $DEMO_SEMAPHORE && rm $DEMO_SEMAPHORE); (test -e $DEMO_RCFILE && rm $DEMO_RCFILE)' EXIT
23+
24+
SESSION=$USER
25+
NESTED_SESSION=${SESSION}_nested
26+
27+
function update_semaphore_token() {
28+
head -c 20 /dev/urandom | xxd -p > "${DEMO_SEMAPHORE}$1"
29+
}
30+
31+
function await_semaphore_token() {
32+
tmux wait-for "$(cat ${DEMO_SEMAPHORE}$1)"
33+
}
34+
35+
function start_tmux_session() {
36+
export DEMO_SEMAPHORE
37+
cat > $DEMO_RCFILE <<'EOF'
38+
PS1='\e[92m»\e[m $(tmux wait-for -S $(cat $DEMO_SEMAPHORE))'
39+
PS2=' \e[92m…\e[m $(tmux wait-for -S $(cat $DEMO_SEMAPHORE))'
40+
EOF
41+
42+
update_semaphore_token
43+
tmux -2 \
44+
new-session \
45+
-x $WIDTH \
46+
-y $HEIGHT \
47+
-d \
48+
-s $SESSION \
49+
asciinema rec -y \
50+
--title="$TITLE" \
51+
--max-wait="$MAX_WAIT" \
52+
--command="/bin/bash --noprofile --rcfile $DEMO_RCFILE" \
53+
$OUTPUT_PATH
54+
}
55+
56+
function type_tmux_keys() {
57+
tmux_target="$1"
58+
keys="$2"
59+
60+
tmux select-pane -t "$tmux_target"
61+
tmux send-keys -t "$tmux_target" "$keys"
62+
}
63+
64+
function type_tmux_line() {
65+
tmux_target="$1"
66+
line="$2"
67+
68+
tmux select-pane -t $tmux_target
69+
70+
eol_key=C-m
71+
if [ "$line" != "#" ]; then
72+
word_delay=$COMMAND_KEY_DELAY
73+
char_delay=$COMMAND_KEY_DELAY
74+
if [ "${line:0:1}" = "#" ]; then
75+
word_delay=$COMMENT_SPACE_DELAY
76+
char_delay=$COMMENT_KEY_DELAY
77+
fi
78+
79+
# Comment out to keep the leading "# " for comments and use
80+
# if [ "${line:0:2}" = "# " ]; then
81+
# line=$(echo -n "$line" | tail -c +3)
82+
# eol_key=C-c
83+
# fi
84+
85+
while IFS='' read -n 1 char; do
86+
if [ "$char" = ' ' ]; then
87+
key_delay=$word_delay
88+
else
89+
key_delay=$char_delay
90+
fi
91+
92+
# For some reason, we need to escape semicolons
93+
if [ "$char" = ';' ]; then
94+
char='\;'
95+
fi
96+
tmux send-keys -t "$tmux_target" -l "$char"
97+
sleep $key_delay
98+
done < <(echo -n "$line")
99+
fi
100+
101+
tmux send-keys -t "$tmux_target" $eol_key
102+
}
103+
104+
function drive_tmux_session() {
105+
tmux_session=$1
106+
tmux_script=$2
107+
108+
has_split=
109+
while IFS= read line; do
110+
if [ "$line" = "" ]; then
111+
sleep $LINE_DELAY
112+
continue
113+
fi
114+
115+
# Figure out which session...
116+
session_index=$(echo "$line" | cut -d' ' -f1)
117+
line="$(echo "$line" | cut -d' ' -f2-)"
118+
119+
if [ "${session_index:0:1}" = "1" ]; then
120+
if [ -z "$has_split" ]; then
121+
has_split=1
122+
update_semaphore_token .1
123+
tmux split-window -t $NESTED_SESSION -h -p 55 env DEMO_SEMAPHORE=${DEMO_SEMAPHORE}.1 /bin/bash --noprofile --rcfile $DEMO_RCFILE
124+
await_semaphore_token .1
125+
fi
126+
fi
127+
128+
# Is this an asynchronous line?
129+
if echo "$session_index" | grep -q -E '^\d+&$'; then
130+
session_index=${session_index:0:${#session_index} - 1}
131+
type_tmux_line $tmux_session.$session_index "$line"
132+
# Or an key line
133+
elif echo "$session_index" | grep -q -E '^\d+E$'; then
134+
session_index=${session_index:0:${#session_index} - 1}
135+
type_tmux_keys $tmux_session.$session_index "$line"
136+
# Is this a well-formed synchronous line?
137+
elif echo "$session_index" | grep -q -E '^\d+$'; then
138+
update_semaphore_token .$session_index
139+
type_tmux_line $tmux_session.$session_index "$line"
140+
await_semaphore_token .$session_index
141+
142+
heredoc_token="$(echo "$line" | grep -E '<<([^ ]+)' | sed -E "s/^.*<<'?([^ ']+).*\$/\\1/")"
143+
if [ -n "$heredoc_token" ]; then
144+
while IFS= read heredoc_line; do
145+
tmux send-keys -t "$tmux_session.$session_index" -l "$heredoc_line"
146+
update_semaphore_token .$session_index
147+
tmux send-keys -t "$tmux_session.$session_index" C-m
148+
await_semaphore_token .$session_index
149+
if [ "$heredoc_line" == "$heredoc_token" ]; then
150+
break
151+
fi
152+
done
153+
fi
154+
else
155+
echo "Malformed line: $line" >&2
156+
exit 1
157+
fi
158+
done < <(tail -n +2 $tmux_script)
159+
160+
sleep $LINE_DELAY
161+
tmux send-keys -t $tmux_session.1 C-d
162+
tmux send-keys -t $tmux_session.0 C-d
163+
}
164+
165+
start_tmux_session
166+
await_semaphore_token
167+
168+
update_semaphore_token .0
169+
tmux send-keys -l "exec tmux new-session -s $NESTED_SESSION env DEMO_SEMAPHORE=${DEMO_SEMAPHORE}.0 /bin/bash --noprofile --rcfile $DEMO_RCFILE ';' set status off"
170+
tmux send-keys C-m
171+
172+
await_semaphore_token .0
173+
174+
drive_tmux_session $NESTED_SESSION $SCRIPT &
175+
176+
tmux set-window-option -t $SESSION force-width $WIDTH
177+
tmux set-window-option -t $SESSION force-height $HEIGHT
178+
tmux set-window-option -t $SESSION aggressive-resize off
179+
180+
# exec tmux attach-session -r -t $SESSION
181+
exec tmux attach-session -t $SESSION

0 commit comments

Comments
 (0)