ruby_raider 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ffmpeg/build.sh +208 -0
- data/.github/ffmpeg/check.sh +72 -0
- data/.github/ffmpeg/frames.rb +34 -0
- data/.github/ffmpeg/probe.sh +17 -0
- data/.github/workflows/ffmpeg.yml +160 -0
- data/README.md +168 -33
- data/RELEASE.md +7 -0
- data/lib/generators/templates/common/read_me.tt +6 -7
- data/lib/generators/templates/helpers/video_helper.tt +1 -2
- data/lib/ruby_raider/ffmpeg.rb +38 -11
- data/lib/version +1 -1
- data/sig/ruby_raider/ffmpeg.rbs +1 -0
- data/spec/generators/video_helper_spec.rb +21 -4
- data/spec/integration/content/readme_content_spec.rb +7 -2
- data/spec/integration/end_to_end_spec.rb +20 -8
- data/spec/utilities/ffmpeg_spec.rb +80 -7
- data/spec/utilities/video_command_spec.rb +13 -0
- metadata +7 -7
- data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +0 -400
- data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +0 -233
- data/docs/superpowers/plans/2026-10-28-failure-card.md +0 -295
- data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +0 -214
- data/docs/superpowers/specs/2026-10-28-failure-card-design.md +0 -210
data/README.md
CHANGED
|
@@ -31,57 +31,186 @@ Ruby Raider is a CLI gem for scaffolding and generating UI test automation frame
|
|
|
31
31
|
|
|
32
32
|
## Supported Frameworks
|
|
33
33
|
|
|
34
|
+
Ruby Raider works on Mac OS, Linux and Windows.
|
|
35
|
+
|
|
34
36
|
### Web Testing
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
Web projects use **Selenium and RSpec**. They get:
|
|
39
|
+
|
|
40
|
+
- Locators people see on the page (`label:`, `text:`, `by_role(...)` and more), with strict matching
|
|
41
|
+
- Automatic waiting for elements and actions: no implicit wait, no `sleep`
|
|
42
|
+
- Retrying RSpec matchers
|
|
43
|
+
- Login reuse, failure video, failure trace, network mocking and screenshot comparison
|
|
44
|
+
- Optional accessibility tests (axe) and a choice of GitHub Actions or Azure Pipelines
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
See [What's Included](#whats-included) for each one.
|
|
41
47
|
|
|
42
|
-
### Mobile Testing
|
|
48
|
+
### Mobile Testing
|
|
49
|
+
|
|
50
|
+
Mobile projects use **Appium and RSpec**:
|
|
43
51
|
|
|
44
52
|
| Test Framework | iOS | Android | Cross-Platform |
|
|
45
53
|
|----------------|-----|---------|----------------|
|
|
46
54
|
| RSpec | ✅ | ✅ | ✅ |
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
They get:
|
|
57
|
+
|
|
58
|
+
- Appium locators: `accessibility_id:`, `id:`, `xpath:` and `class_name:`
|
|
59
|
+
- Automatic waiting: `find_element` waits for a match, and `click`, `send_keys` and `clear` wait until the element is displayed and enabled
|
|
60
|
+
- Retrying RSpec matchers
|
|
61
|
+
- Gestures: `swipe`, `scroll` and `scroll_to`, on iOS and Android
|
|
62
|
+
- Failure video from Appium's screen recording
|
|
63
|
+
- Raider reporting and a GitHub Actions workflow that starts a simulator or emulator and Appium
|
|
64
|
+
|
|
65
|
+
On native app screens, `find_element` returns the first match (strict matching is web only) and `submit` is not checked.
|
|
66
|
+
|
|
67
|
+
To run the examples, download the example [app](https://github.com/RaiderHQ/raider_test_app) and start the server:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
raider u start_appium
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Optional Add-ons
|
|
49
74
|
|
|
50
75
|
| Add-on | Flag | Description |
|
|
51
76
|
|--------|------|-------------|
|
|
52
|
-
| Accessibility | `--accessibility` | Adds axe gem
|
|
53
|
-
| CI provider | `-p ci:azure` | Generates `azure-pipelines.yml` instead of the GitHub Actions workflow (default `ci:github`) |
|
|
54
|
-
|
|
55
|
-
This works on all platforms (Mac OS, Linux and Windows).
|
|
77
|
+
| Accessibility (web) | `--accessibility` | Adds the axe gem and example accessibility tests |
|
|
78
|
+
| CI provider (web) | `-p ci:azure` | Generates `azure-pipelines.yml` instead of the GitHub Actions workflow (default `ci:github`) |
|
|
56
79
|
|
|
57
80
|
## What's Included
|
|
58
81
|
|
|
59
|
-
Every generated project
|
|
60
|
-
|
|
61
|
-
- **Page Object Model** — Base page and component classes with example pages
|
|
62
|
-
- **Test examples** — Working login tests against the [Raider Test Store](https://raider-test-site.onrender.com/)
|
|
63
|
-
- **Raider reporting** — Generated projects include [raider_reporter](https://github.com/RaiderHQ/raider_reporter), which writes results to `raider-results/` and uploads runs to your Raider Hideout server when `RAIDER_TOKEN` and `RAIDER_ENDPOINT` are set; generated CI runs the suite through `raider-reporter exec` with `RAIDER_QUARANTINE`, so quarantined tests do not fail the build; an opt-in `raider-gate` job (`RAIDER_GATE=1`) fails CI when the Raider quality gate fails
|
|
64
|
-
- **Locators, waiting and assertions**: The runtime (`ruby_raider/runtime`), loaded by every generated project, adds waiting to Selenium and Appium themselves. Locators go beyond `id` and `css`: `label:`, `placeholder:`, `alt:`, `title:`, `text:`, `test_id:` and `by_role('button', name: 'Save')` find elements the way a person sees them, and `find_element` fails when a locator matches more than one. `find_element` and `find_elements` wait for a match, so there is no implicit wait and no `sleep`. Before an action the runtime waits until the element is ready: `click` waits until it is visible, enabled, not inert, stable and not covered; `send_keys` and `clear` until it is visible, enabled, editable and not inert; `submit` until it is visible, enabled and not inert (on native apps, displayed and enabled); `force: true` skips the checks for one call. `timeout`, `strict` and `actionability` in `config.yml` (or `RAIDER_TIMEOUT`, `RAIDER_STRICT`, `RAIDER_ACTIONABILITY`) set the rules. RSpec matchers (`have_text`, `have_value`, `have_attribute`, `have_css`, `have_xpath`, `be_visible`, `be_hidden`, `be_enabled`, `be_disabled`, `be_checked`, `have_count`, `have_title`, `have_url`, `have_element`) retry until the page matches, and a block expectation such as `expect { driver.find_element(id: 'status').text }.to eq('done')` retries with any value matcher
|
|
65
|
-
- **Example tests or a skeleton (`examples:false`)** — start from the working login examples, or pass `examples:false` for an empty skeleton with the helpers, config, CI and README (see [Getting Started](#getting-started))
|
|
66
|
-
- **Login reuse** — generated web projects (Selenium) log in once and restore the saved session for examples tagged `:logged_in`; `RAIDER_LOGIN_STATE=off|refresh` and `login_state: max_age:` control it, and the state stays in git-ignored `.raider/auth/`
|
|
67
|
-
- **Failure video replay** — Every test is recorded and the video is kept only for failing tests, attached to the report as `video` (WebM from page screenshots for Selenium, Appium's own MP4 for mobile, which needs `ffmpeg` on the Appium server host for iOS and for Android recordings over 180 s). Turn it off per run with `VIDEO=off`, or per project with `raider u video off`; ffmpeg is downloaded once, SHA-256 checked and cached in `~/.cache/raider`. The RaiderHQ `ffmpeg-7.1` build that the download uses is not published yet, so for now set `RAIDER_FFMPEG_PATH` to an ffmpeg with libvpx to get web videos
|
|
68
|
-
- **Failure trace replay** (Selenium) — Failing tests keep a step-by-step trace: each action with its locator and wait time, a screenshot and the page HTML after every step that changes the page, plus network requests and console messages on Chrome, Edge and Firefox. It is saved as `raider-results/attachments/<n>-trace.zip`, attached to the report as `trace` and opened in Raider Desktop or on Hideout. `TRACE=on` keeps passing tests too, `TRACE=off` captures nothing, and `raider u trace on|off` sets it for the project. Typed values in password fields and `RAIDER_SECRET_*` values are hidden; other typed values, page text and URLs are kept, so treat a trace like the video. Request headers, cookies and bodies are never captured
|
|
69
|
-
- **Network mocking** (Selenium) — Answer a request with your own response or block it, from the test: `mock_request('**/api/users', json: { users: [] })`, `mock_request(url, status: 500)` and `block_request('**/*.png')`, with `calls` to check what the page asked for. It uses WebDriver BiDi on Chrome, Edge and Firefox (Safari and `BIDI=off` raise `NetworkHelper::Unavailable`), handles CORS for cross-origin mocks, clears mocks after each test, and marks mocked requests in the trace
|
|
70
|
-
- **Screenshot comparison** (Selenium) — `match_screenshot('home')` compares the page, or an element with `element:`, with a baseline in `screenshots/baselines/<browser>/`. The first run saves the baseline; later runs fail on a difference and attach the expected, actual and diff pictures to the report and the trace. `SCREENSHOTS=update` rewrites baselines, a missing baseline fails on CI, and tolerances (`threshold`, `max_diff_pixels`, `max_diff_ratio`) go in `config/config.yml` under `screenshots:`. Generate baselines where CI runs: a Retina laptop makes 2x pictures
|
|
71
|
-
- **GitHub Actions CI** — Ready-to-use test pipeline that caches ffmpeg for failure videos
|
|
72
|
-
- **Rake tasks** — Default, smoke, and regression tasks with tag-based filtering
|
|
73
|
-
- **Retry support** — RSpec retry with configurable retry count via `RETRY_COUNT` env var
|
|
74
|
-
- **Re-run failed tests** — RSpec keeps `spec/examples.txt` for `--only-failures`
|
|
75
|
-
- **Parallel execution** — parallel_tests and parallel_split_test gems included
|
|
76
|
-
- **Configurable browser** — Chrome/Firefox with customizable arguments, viewport, timeout, and headless mode
|
|
77
|
-
- **Linting** — RuboCop and Reek configs included
|
|
78
|
-
|
|
79
|
-
For Appium projects, download the example [app](https://github.com/RaiderHQ/raider_test_app) and start the server:
|
|
82
|
+
Every generated project loads the Raider runtime (`ruby_raider/runtime`). It adds locators, waiting and assertions to Selenium and Appium themselves.
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
### Locators
|
|
85
|
+
|
|
86
|
+
Find elements the way a person sees them, on top of Selenium's `id:`, `css:`, `xpath:`, `name:`, `class:` and `link_text:`:
|
|
87
|
+
|
|
88
|
+
| Key | Finds |
|
|
89
|
+
|-----|-------|
|
|
90
|
+
| `label:` | the field with that label, `aria-label` or `aria-labelledby` |
|
|
91
|
+
| `placeholder:` | the field with that placeholder |
|
|
92
|
+
| `alt:` | the element with that `alt` text |
|
|
93
|
+
| `title:` | the element with that `title` |
|
|
94
|
+
| `text:` | the innermost element whose text contains it |
|
|
95
|
+
| `test_id:` | the element whose `data-testid` equals it |
|
|
96
|
+
| `by_role(...)` | the element with that ARIA role and name |
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
driver.find_element(label: 'Email')
|
|
100
|
+
driver.find_element(label: ['Email', { exact: true }]) # whole text only
|
|
101
|
+
driver.find_element(by_role('button', name: 'Save'))
|
|
102
|
+
driver.find_element(by_role('heading', level: 1))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Text keys match part of the text and ignore extra spaces. Pass `{ exact: true }` for the whole text.
|
|
106
|
+
- `test_id:` reads another attribute with `Raider.test_id_attribute = 'data-qa'` or `RAIDER_TEST_ID_ATTRIBUTE`.
|
|
107
|
+
- **Strict matching:** `find_element` fails with `Raider::AmbiguousMatch` when a locator matches more than one element. `find_elements` never does. Turn it off with `raider u strict off`.
|
|
108
|
+
|
|
109
|
+
These keys are for web pages. Mobile projects use Appium's locators (see [Mobile Testing](#mobile-testing)).
|
|
110
|
+
|
|
111
|
+
### Waiting and actionability
|
|
112
|
+
|
|
113
|
+
- `find_element` and `find_elements` wait for a match. There is no implicit wait and no `sleep`.
|
|
114
|
+
- Before an action, the runtime waits until the element is ready:
|
|
115
|
+
|
|
116
|
+
| Action | Waits until the element is |
|
|
117
|
+
|--------|----------------------------|
|
|
118
|
+
| `click` | visible, enabled, not inert, stable and not covered |
|
|
119
|
+
| `send_keys`, `clear` | visible, enabled, editable and not inert |
|
|
120
|
+
| `submit` | visible, enabled and not inert |
|
|
121
|
+
|
|
122
|
+
- `force: true` skips the checks for one call: `button.click(force: true)`.
|
|
123
|
+
- Set the rules with `timeout`, `strict` and `actionability` in `config/config.yml`, or `RAIDER_TIMEOUT`, `RAIDER_STRICT` and `RAIDER_ACTIONABILITY`.
|
|
124
|
+
|
|
125
|
+
### Assertions
|
|
126
|
+
|
|
127
|
+
RSpec matchers retry until the page matches or `timeout` runs out:
|
|
128
|
+
|
|
129
|
+
`have_text`, `have_value`, `have_attribute`, `have_css`, `have_xpath`, `have_element`, `have_count`, `have_title`, `have_url`, `be_visible`, `be_hidden`, `be_enabled`, `be_disabled`, `be_checked`
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
expect { account_page.welcome_message }.to have_text('Welcome')
|
|
133
|
+
expect { driver.find_element(id: 'status').text }.to eq('done') # any value matcher retries in a block
|
|
83
134
|
```
|
|
84
135
|
|
|
136
|
+
### Raider reporting
|
|
137
|
+
|
|
138
|
+
Projects include [raider_reporter](https://github.com/RaiderHQ/raider_reporter).
|
|
139
|
+
|
|
140
|
+
- Results go to `raider-results/`.
|
|
141
|
+
- Runs upload to your Raider Hideout server when `RAIDER_TOKEN` and `RAIDER_ENDPOINT` are set.
|
|
142
|
+
- CI runs the suite through `raider-reporter exec` with `RAIDER_QUARANTINE`, so quarantined tests do not fail the build.
|
|
143
|
+
- Set the CI variable `RAIDER_GATE` to `1` to add a `raider-gate` job that fails CI when the Raider quality gate fails.
|
|
144
|
+
|
|
145
|
+
### Failure video replay
|
|
146
|
+
|
|
147
|
+
Each test is recorded. The video is kept only when the test fails and attached to the report as `video`.
|
|
148
|
+
|
|
149
|
+
- **Web:** WebM built from page screenshots. ffmpeg is downloaded once, SHA-256 checked and cached in `~/.cache/raider`: an LGPL ffmpeg 7.1 build with libvpx for macOS (Apple silicon and Intel), Linux x64 and Windows x64, built by [`.github/workflows/ffmpeg.yml`](.github/workflows/ffmpeg.yml). On other platforms set `RAIDER_FFMPEG_PATH` to an ffmpeg with libvpx.
|
|
150
|
+
- **Mobile:** Appium's own MP4. iOS needs `ffmpeg` on the Appium server host. Android stops at 180 s by default, so it needs no ffmpeg; a longer `video.time_limit` needs ffmpeg too.
|
|
151
|
+
- Turn it off per run with `VIDEO=off`, or per project with `raider u video off`.
|
|
152
|
+
|
|
153
|
+
### Failure trace replay (web)
|
|
154
|
+
|
|
155
|
+
Failing tests keep a step-by-step trace:
|
|
156
|
+
|
|
157
|
+
- each action with its locator and wait time
|
|
158
|
+
- a screenshot and the page HTML after every step that changes the page
|
|
159
|
+
- network requests and console messages on Chrome, Edge and Firefox
|
|
160
|
+
|
|
161
|
+
It is saved as `raider-results/attachments/<n>-trace.zip`, attached to the report as `trace`, and opens in [Raider Desktop](https://ruby-raider.com) or on Hideout.
|
|
162
|
+
|
|
163
|
+
- `TRACE=on` keeps passing tests too. `TRACE=off` captures nothing. `raider u trace on|off` sets it for the project.
|
|
164
|
+
- Password fields and `RAIDER_SECRET_*` values are hidden. Other typed values, page text and URLs are kept, so treat a trace like the video.
|
|
165
|
+
- Request headers, cookies and bodies are never captured.
|
|
166
|
+
|
|
167
|
+
### Network mocking (web)
|
|
168
|
+
|
|
169
|
+
Answer a request with your own response, or block it:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
mock_request('**/api/users', json: { users: [] })
|
|
173
|
+
mock_request(url, status: 500)
|
|
174
|
+
block_request('**/*.png')
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `mock_request` returns the mock; `mock.calls` lists the requests it answered.
|
|
178
|
+
- Works through WebDriver BiDi on Chrome, Edge and Firefox. Safari and `BIDI=off` raise `NetworkHelper::Unavailable`.
|
|
179
|
+
- Handles CORS for cross-origin mocks, clears mocks after each test, and marks mocked requests in the trace.
|
|
180
|
+
|
|
181
|
+
### Screenshot comparison (web)
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
match_screenshot('home')
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- Compares the page, or one element with `element:`, with a baseline in `screenshots/baselines/<browser>/`.
|
|
188
|
+
- The first run saves the baseline. Later runs fail on a difference and attach the expected, actual and diff pictures to the report and the trace.
|
|
189
|
+
- `SCREENSHOTS=update` rewrites baselines. A missing baseline fails on CI.
|
|
190
|
+
- Tolerances (`threshold`, `max_diff_pixels`, `max_diff_ratio`) go in `config/config.yml` under `screenshots:`.
|
|
191
|
+
- Generate baselines where CI runs: a Retina laptop makes 2x pictures.
|
|
192
|
+
|
|
193
|
+
### Login reuse (web)
|
|
194
|
+
|
|
195
|
+
Example projects log in once and restore the saved session for examples tagged `:logged_in`.
|
|
196
|
+
|
|
197
|
+
- `RAIDER_LOGIN_STATE=off|refresh` and `login_state: max_age:` control it.
|
|
198
|
+
- The state stays in git-ignored `.raider/auth/`.
|
|
199
|
+
- Skeletons (`examples:false`) get `SessionState#reuse` to wire your own login.
|
|
200
|
+
|
|
201
|
+
### Also included
|
|
202
|
+
|
|
203
|
+
- **Page Object Model:** base page and component classes with example pages
|
|
204
|
+
- **Example tests:** working tests against the [Raider Test Store](https://raider-test-site.onrender.com/) (web) or the [Raider test app](https://github.com/RaiderHQ/raider_test_app) (mobile), or an empty skeleton with `examples:false` (see [Getting Started](#getting-started))
|
|
205
|
+
- **CI:** GitHub Actions, or Azure Pipelines for web. It caches ffmpeg and comments results on pull requests
|
|
206
|
+
- **Failure screenshots:** each failing test attaches a screenshot to the report
|
|
207
|
+
- **Rake tasks:** default, smoke and regression, filtered by tag
|
|
208
|
+
- **Retries:** RSpec retry, with the count set by `RETRY_COUNT`
|
|
209
|
+
- **Re-run failed tests:** RSpec keeps `spec/examples.txt` for `--only-failures`
|
|
210
|
+
- **Parallel execution:** `parallel_tests` and `parallel_split_test`
|
|
211
|
+
- **Configurable browser:** Chrome or Firefox, with arguments, viewport, timeout and headless mode
|
|
212
|
+
- **Linting:** RuboCop and Reek configs
|
|
213
|
+
|
|
85
214
|
## Getting Started
|
|
86
215
|
|
|
87
216
|
Install the gem:
|
|
@@ -102,6 +231,12 @@ Or skip the menu with parameters:
|
|
|
102
231
|
raider new [project_name] -p framework:rspec automation:selenium
|
|
103
232
|
```
|
|
104
233
|
|
|
234
|
+
Create a mobile project (`automation:ios`, `automation:android` or `automation:cross_platform`):
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
raider new my_app -p framework:rspec automation:android
|
|
238
|
+
```
|
|
239
|
+
|
|
105
240
|
Add accessibility testing:
|
|
106
241
|
|
|
107
242
|
```bash
|
data/RELEASE.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Release Notes
|
|
4
4
|
|
|
5
|
+
### 4.0.1
|
|
6
|
+
|
|
7
|
+
- Failure video works for web projects without `RAIDER_FFMPEG_PATH`: the RaiderHQ `ffmpeg-7.1` release is published and `RubyRaider::Ffmpeg::MANIFEST` pins the SHA-256 of its four binaries (macOS arm64 and x64, Linux x64, Windows x64). The first failing test, or `raider u video install`, downloads the one for your platform, checks it and caches it in `~/.cache/raider` (`%LOCALAPPDATA%\raider` on Windows).
|
|
8
|
+
- The binaries are FFmpeg 7.1.5 built from source by `.github/workflows/ffmpeg.yml` (`.github/ffmpeg/build.sh`): LGPL (no `--enable-gpl`, no `--enable-nonfree`), statically linked with libvpx 1.15.2 and zlib 1.3.1, with only the PNG input, scaling, VP8/VP9 encoding and WebM output that the video helper uses. The release carries the license texts, the source tarballs and the exact configure line. macOS binaries are ad-hoc signed, not notarized (`spctl` rejects them). Gatekeeper only checks files carrying the quarantine attribute, which a browser download gets and the gem's Net::HTTP download does not, so the gem's copy runs; a copy downloaded with a browser would be blocked.
|
|
9
|
+
- On other platforms (Linux arm64, for example) `RAIDER_FFMPEG_PATH` is still the way to get web videos; the `[video]` line now says so.
|
|
10
|
+
- Existing 4.0.0 projects need no change beyond `bundle update ruby_raider`.
|
|
11
|
+
|
|
5
12
|
### 4.0.0
|
|
6
13
|
|
|
7
14
|
- Watir is removed: `automation:watir` is no longer a valid automation value.
|
|
@@ -746,17 +746,16 @@ The video is a `video.webm` built from page screenshots with ffmpeg.
|
|
|
746
746
|
`raider u video off` (`raider u video on` turns it back on).
|
|
747
747
|
<%- end -%>
|
|
748
748
|
<%- unless mobile? -%>
|
|
749
|
-
- ffmpeg
|
|
750
|
-
(`%LOCALAPPDATA%\raider` on Windows).
|
|
749
|
+
- ffmpeg 7.1 (an LGPL build with libvpx, from the RaiderHQ `ffmpeg-7.1` release) is downloaded on first use,
|
|
750
|
+
checked against a pinned SHA-256 and cached in `~/.cache/raider` (`%LOCALAPPDATA%\raider` on Windows). Builds exist
|
|
751
|
+
for macOS (Apple silicon and Intel), Linux x64 and Windows x64. `raider u video install` downloads it ahead of time (CI does this before the
|
|
751
752
|
tests and caches the folder), and `raider u video status` prints the binary in use.
|
|
752
|
-
- `RAIDER_FFMPEG_PATH` uses your own ffmpeg (it needs libvpx) instead of the download,
|
|
753
|
-
the cache.
|
|
753
|
+
- `RAIDER_FFMPEG_PATH` uses your own ffmpeg (it needs libvpx) instead of the download, for example on Linux arm64,
|
|
754
|
+
and `RAIDER_CACHE_DIR` moves the cache.
|
|
754
755
|
<%- if azure? -%>
|
|
755
756
|
- `azure-pipelines.yml` sets `RAIDER_CACHE_DIR` to `$(Pipeline.Workspace)/.cache/raider`, the folder its `Cache@2`
|
|
756
757
|
step keeps between runs.
|
|
757
758
|
<%- end -%>
|
|
758
|
-
- The RaiderHQ `ffmpeg-7.1` release that the download comes from is not published yet. Until it is, set
|
|
759
|
-
`RAIDER_FFMPEG_PATH` to get videos; without it the suite runs as usual and prints one `[video]` line.
|
|
760
759
|
- If ffmpeg is missing or recording fails, the suite still runs and its exit code is unchanged; you get one `[video]`
|
|
761
760
|
line on stderr and no video.
|
|
762
761
|
- Known limits: a frame is taken after each page load, click and text change, plus one at the end of a failing test,
|
|
@@ -777,7 +776,7 @@ The video is a `video.webm` built from page screenshots with ffmpeg.
|
|
|
777
776
|
than 180 s needs ffmpeg on the Appium host.
|
|
778
777
|
- `raider u video install` and `raider u video status` manage the ffmpeg that web projects use, cached in
|
|
779
778
|
`~/.cache/raider` (override with `RAIDER_FFMPEG_PATH` or `RAIDER_CACHE_DIR`); they do not install ffmpeg on the
|
|
780
|
-
Appium host.
|
|
779
|
+
Appium host.
|
|
781
780
|
- If recording fails, the suite still runs and its exit code is unchanged; you get one `[video]` line on stderr and
|
|
782
781
|
no video.
|
|
783
782
|
<%- end -%>
|
|
@@ -24,8 +24,7 @@ module VideoHelper
|
|
|
24
24
|
<%- unless mobile? -%>
|
|
25
25
|
MAX_FRAME_SECONDS = 2.0
|
|
26
26
|
ENCODE_TIMEOUT = 120
|
|
27
|
-
NO_FFMPEG = 'no ffmpeg
|
|
28
|
-
'`raider u video install` once the ffmpeg release is published)'
|
|
27
|
+
NO_FFMPEG = 'no ffmpeg download for this platform (set RAIDER_FFMPEG_PATH to an ffmpeg with libvpx)'
|
|
29
28
|
<%- end -%>
|
|
30
29
|
|
|
31
30
|
@disabled = false
|
data/lib/ruby_raider/ffmpeg.rb
CHANGED
|
@@ -14,16 +14,31 @@ module RubyRaider
|
|
|
14
14
|
MAX_REDIRECTS = 5
|
|
15
15
|
# Seconds for the whole download, redirects included.
|
|
16
16
|
DOWNLOAD_TIMEOUT = 300
|
|
17
|
+
# The binaries are 3-5 MB; a bigger body is not ours and must not fill the disk before the checksum.
|
|
18
|
+
MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024
|
|
17
19
|
|
|
18
|
-
#
|
|
20
|
+
# The RaiderHQ `ffmpeg-7.1` release: FFmpeg 7.1.5, LGPL, built by .github/workflows/ffmpeg.yml
|
|
21
|
+
# (.github/ffmpeg/build.sh), run 42440762165. Each SHA-256 pins the exact bytes of that release's asset.
|
|
19
22
|
RELEASE_BASE = 'https://github.com/RaiderHQ/ruby_raider/releases/download/ffmpeg-7.1/ffmpeg-7.1-'
|
|
20
23
|
MANIFEST = {
|
|
21
24
|
version: '7.1',
|
|
22
25
|
assets: {
|
|
23
|
-
'darwin-arm64' => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
'darwin-arm64' => {
|
|
27
|
+
url: "#{RELEASE_BASE}darwin-arm64".freeze,
|
|
28
|
+
sha256: 'e6c9240c133f22ced9f1e55fc2d0889991274e1b787c296583cb74f7242ef417'
|
|
29
|
+
}.freeze,
|
|
30
|
+
'darwin-x64' => {
|
|
31
|
+
url: "#{RELEASE_BASE}darwin-x64".freeze,
|
|
32
|
+
sha256: 'e649ff3c5abe05700a657a12d549ee27ac53e190cd1aaee7a113765d3189e91f'
|
|
33
|
+
}.freeze,
|
|
34
|
+
'linux-x64' => {
|
|
35
|
+
url: "#{RELEASE_BASE}linux-x64".freeze,
|
|
36
|
+
sha256: '108b685bbfba103c8f25c641fc4c21e5ea79acecd131f9ea964db959e894e7d4'
|
|
37
|
+
}.freeze,
|
|
38
|
+
'windows-x64' => {
|
|
39
|
+
url: "#{RELEASE_BASE}windows-x64.exe".freeze,
|
|
40
|
+
sha256: 'e27c4d32675e8715a6b3260c87ed3834f959275af0412f501ed5781d5324ac1a'
|
|
41
|
+
}.freeze
|
|
27
42
|
}.freeze
|
|
28
43
|
}.freeze
|
|
29
44
|
|
|
@@ -115,12 +130,8 @@ module RubyRaider
|
|
|
115
130
|
def handle_response(response, destination, deadline, timeout)
|
|
116
131
|
case response
|
|
117
132
|
when Net::HTTPSuccess
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
check_deadline(deadline, timeout)
|
|
121
|
-
file.write(chunk)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
133
|
+
check_size(response['content-length'].to_i)
|
|
134
|
+
write_body(response, destination, deadline, timeout)
|
|
124
135
|
nil
|
|
125
136
|
when Net::HTTPRedirection
|
|
126
137
|
response['location'] || raise(IOError, 'redirect without Location header')
|
|
@@ -129,6 +140,22 @@ module RubyRaider
|
|
|
129
140
|
end
|
|
130
141
|
end
|
|
131
142
|
|
|
143
|
+
def write_body(response, destination, deadline, timeout)
|
|
144
|
+
written = 0
|
|
145
|
+
File.open(destination, 'wb') do |file|
|
|
146
|
+
response.read_body do |chunk|
|
|
147
|
+
check_deadline(deadline, timeout)
|
|
148
|
+
written += chunk.bytesize
|
|
149
|
+
check_size(written)
|
|
150
|
+
file.write(chunk)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def check_size(bytes)
|
|
156
|
+
raise IOError, "download larger than #{MAX_DOWNLOAD_BYTES} bytes" if bytes > MAX_DOWNLOAD_BYTES
|
|
157
|
+
end
|
|
158
|
+
|
|
132
159
|
def socket_timeouts(deadline, timeout)
|
|
133
160
|
left = check_deadline(deadline, timeout)
|
|
134
161
|
{ open_timeout: [10, left].min, read_timeout: [60, left].min }
|
data/lib/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.1
|
data/sig/ruby_raider/ffmpeg.rbs
CHANGED
|
@@ -128,6 +128,19 @@ describe 'Generated VideoHelper' do
|
|
|
128
128
|
expect(argv.each_cons(2).to_a).to include(['-vf', "scale='min(1280,iw)':-2"])
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# The pinned ffmpeg has only what this encode needs (.github/ffmpeg/build.sh), and
|
|
132
|
+
# .github/ffmpeg/check.sh proves it on every build: both must run the same arguments.
|
|
133
|
+
it 'runs the encode that .github/ffmpeg/check.sh proves on the pinned ffmpeg builds' do
|
|
134
|
+
recorder = VideoHelper.recorder_for(driver)
|
|
135
|
+
recorder.start
|
|
136
|
+
recorder.stop(keep: true)
|
|
137
|
+
|
|
138
|
+
check = File.read(File.expand_path('../../.github/ffmpeg/check.sh', __dir__)).gsub(/\\\n\s*/, '')
|
|
139
|
+
shell = argv[0..-2].map { |arg| arg.include?("'") ? %("#{arg}") : arg }
|
|
140
|
+
shell[shell.index('-i') + 1] = '"$work/list.txt"'
|
|
141
|
+
expect(check).to include(%("$binary" #{shell.join(' ')} "$work/video.webm"))
|
|
142
|
+
end
|
|
143
|
+
|
|
131
144
|
context 'when the config sets other fps and max_width' do
|
|
132
145
|
let(:video_config) { { 'enabled' => true, 'fps' => 10, 'max_width' => 640 } }
|
|
133
146
|
|
|
@@ -351,9 +364,14 @@ describe 'Generated VideoHelper' do
|
|
|
351
364
|
end
|
|
352
365
|
end
|
|
353
366
|
|
|
354
|
-
context 'when no ffmpeg can be obtained (no override,
|
|
367
|
+
context 'when no ffmpeg can be obtained (no override, no build for this platform)' do
|
|
355
368
|
let(:env) { { 'RAIDER_FFMPEG_PATH' => nil } }
|
|
356
369
|
|
|
370
|
+
before do
|
|
371
|
+
require 'ruby_raider/ffmpeg'
|
|
372
|
+
stub_const('RubyRaider::Ffmpeg::MANIFEST', { version: '7.1', assets: {} })
|
|
373
|
+
end
|
|
374
|
+
|
|
357
375
|
it 'decides once, at the first call: listener nil, NullRecorder, exactly one [video] line' do
|
|
358
376
|
require 'ruby_raider/ffmpeg'
|
|
359
377
|
allow(RubyRaider::Ffmpeg).to receive(:ensure!)
|
|
@@ -362,9 +380,8 @@ describe 'Generated VideoHelper' do
|
|
|
362
380
|
expect(warnings.size).to eq(1)
|
|
363
381
|
expect(VideoHelper.recorder_for(driver)).to be_a(VideoHelper::NullRecorder)
|
|
364
382
|
expect(VideoHelper.listener).to be_nil
|
|
365
|
-
expect(warnings).to eq(['[video] no ffmpeg
|
|
366
|
-
|
|
367
|
-
"recording disabled for this run\n"])
|
|
383
|
+
expect(warnings).to eq(['[video] no ffmpeg download for this platform (set RAIDER_FFMPEG_PATH to an ffmpeg ' \
|
|
384
|
+
"with libvpx); recording disabled for this run\n"])
|
|
368
385
|
expect(RubyRaider::Ffmpeg).not_to have_received(:ensure!)
|
|
369
386
|
expect(shots).to be_empty
|
|
370
387
|
end
|
|
@@ -17,8 +17,13 @@ describe 'Generated Readme content' do
|
|
|
17
17
|
'RAIDER_CACHE_DIR', 'raider-results/attachments/')
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
it '
|
|
21
|
-
expect(section).to include('ffmpeg-7.1'
|
|
20
|
+
it 'names the ffmpeg-7.1 release and no longer says it is unpublished' do
|
|
21
|
+
expect(section).to include('ffmpeg-7.1') if web
|
|
22
|
+
expect(section).not_to include('not published yet')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'lists the platforms with an ffmpeg download for web projects' do
|
|
26
|
+
expect(section).to include('macOS (Apple silicon and Intel), Linux x64 and Windows x64') if web
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
it 'names the right video format and how to turn video off for this project' do
|
|
@@ -321,9 +321,9 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
321
321
|
JSON.parse(File.read(File.join(project, 'raider-results', 'run.json')))['results']
|
|
322
322
|
end
|
|
323
323
|
|
|
324
|
-
# Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH),
|
|
325
|
-
#
|
|
326
|
-
# pointed at a specific binary with FIREFOX_BINARY.
|
|
324
|
+
# Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), so most examples
|
|
325
|
+
# never download the release binary; RAIDER_E2E_DOWNLOAD=1 adds one that does. Skipped when it, or the browser,
|
|
326
|
+
# is missing. Firefox can be pointed at a specific binary with FIREFOX_BINARY.
|
|
327
327
|
context 'Failure video replay', :slow, :video do
|
|
328
328
|
let(:ffmpeg_path) { ENV.fetch('RAIDER_FFMPEG_PATH', '') }
|
|
329
329
|
let(:cache_dir) { Dir.mktmpdir('raider-cache') }
|
|
@@ -421,16 +421,28 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
421
421
|
it 'runs unchanged with one [video] line and no download when no ffmpeg is usable', :aggregate_failures do
|
|
422
422
|
off = run_in_project(project, command, env: video_env('VIDEO' => 'off'))
|
|
423
423
|
FileUtils.rm_rf(File.join(project, 'raider-results'))
|
|
424
|
-
|
|
424
|
+
# A file where the cache dir should be: the download cannot even start, so nothing is fetched.
|
|
425
|
+
blocker = File.join(cache_dir, 'not-a-dir')
|
|
426
|
+
File.write(blocker, '')
|
|
427
|
+
result = run_in_project(project, command, env: video_env('RAIDER_FFMPEG_PATH' => nil, 'RAIDER_CACHE_DIR' => blocker))
|
|
425
428
|
|
|
426
429
|
expect(result[:exit_code]).to eq(off[:exit_code]), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
427
430
|
video_lines = result[:stderr].lines.grep(/\[video\]/)
|
|
428
431
|
expect(video_lines.size).to eq(1), "STDERR: #{result[:stderr]}"
|
|
429
|
-
|
|
430
|
-
# manifest has no checksum, so nothing is captured or downloaded.
|
|
431
|
-
expect(video_lines.first).to include('no ffmpeg available for this platform yet')
|
|
432
|
+
expect(video_lines.first).to include('ffmpeg download failed')
|
|
432
433
|
expect(video_files(project)).to be_empty
|
|
433
|
-
expect(Dir.glob(File.join(cache_dir, '**', '*'))).to
|
|
434
|
+
expect(Dir.glob(File.join(cache_dir, '**', '*'))).to eq([blocker])
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Downloads the pinned ffmpeg-7.1 release asset for this platform into an empty cache.
|
|
438
|
+
it 'downloads the pinned ffmpeg into an empty cache and attaches the video', :aggregate_failures do
|
|
439
|
+
skip 'set RAIDER_E2E_DOWNLOAD=1 to download the ffmpeg-7.1 release asset' unless ENV['RAIDER_E2E_DOWNLOAD'] == '1'
|
|
440
|
+
|
|
441
|
+
result = run_in_project(project, command, env: video_env('RAIDER_FFMPEG_PATH' => nil))
|
|
442
|
+
expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
443
|
+
expect(result[:stderr]).not_to include('[video]')
|
|
444
|
+
expect(Dir.glob(File.join(cache_dir, 'ffmpeg', '7.1', 'ffmpeg{,.exe}')).size).to eq(1)
|
|
445
|
+
expect_video_for_failure_only(project, 'fails after driving the page', result)
|
|
434
446
|
end
|
|
435
447
|
end
|
|
436
448
|
end
|
|
@@ -41,11 +41,51 @@ describe RubyRaider::Ffmpeg do
|
|
|
41
41
|
expect(described_class.platform(host_os: 'linux-gnu', host_cpu: 'aarch64')).to be_nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
it '
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
expect(assets.
|
|
48
|
-
|
|
44
|
+
it 'pins a SHA-256 and the ffmpeg-7.1 release url for each of the four platforms', :aggregate_failures do
|
|
45
|
+
expect(described_class::MANIFEST[:version]).to eq('7.1')
|
|
46
|
+
base = 'https://github.com/RaiderHQ/ruby_raider/releases/download/ffmpeg-7.1/ffmpeg-7.1-'
|
|
47
|
+
expect(described_class::MANIFEST[:assets].transform_values { |a| a[:url] }).to eq(
|
|
48
|
+
'darwin-arm64' => "#{base}darwin-arm64", 'darwin-x64' => "#{base}darwin-x64",
|
|
49
|
+
'linux-x64' => "#{base}linux-x64", 'windows-x64' => "#{base}windows-x64.exe"
|
|
50
|
+
)
|
|
51
|
+
shas = described_class::MANIFEST[:assets].values.map { |a| a[:sha256] }
|
|
52
|
+
expect(shas).to all(match(/\A\h{64}\z/))
|
|
53
|
+
expect(shas.uniq.size).to eq(4)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe 'the pinned checksums' do
|
|
57
|
+
let(:real) { described_class::MANIFEST }
|
|
58
|
+
|
|
59
|
+
it 'is obtainable on every supported platform without an override' do
|
|
60
|
+
described_class::PLATFORMS.each do |p|
|
|
61
|
+
expect(described_class.obtainable?(env: {}, platform: p)).to be(true)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'discards a download whose bytes do not match the pinned checksum', :aggregate_failures do
|
|
66
|
+
described_class::PLATFORMS.each do |p|
|
|
67
|
+
result = described_class.ensure!(env:, manifest: real, fetch:, warn: warner, platform: p)
|
|
68
|
+
expect(result).to be_nil
|
|
69
|
+
end
|
|
70
|
+
expect(calls).to eq(real[:assets].values.map { |a| a[:url] })
|
|
71
|
+
expect(warnings).to all(include('checksum mismatch'))
|
|
72
|
+
expect(Dir.glob(File.join(cache, '**', '*'))).to be_empty
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# FFMPEG_ARTIFACTS=<dir> holding ffmpeg-7.1-<platform>[.exe] (the build workflow's artifacts
|
|
76
|
+
# or the release assets) proves those exact bytes pass verification and get cached.
|
|
77
|
+
it 'accepts the built binaries', unless: ENV.fetch('FFMPEG_ARTIFACTS', '').empty? do
|
|
78
|
+
described_class::PLATFORMS.each do |p|
|
|
79
|
+
name = File.basename(real[:assets][p][:url])
|
|
80
|
+
source = File.join(ENV.fetch('FFMPEG_ARTIFACTS'), name)
|
|
81
|
+
copy = ->(_url, dest) { FileUtils.cp(source, dest) }
|
|
82
|
+
own = { 'RAIDER_CACHE_DIR' => File.join(cache, p) }
|
|
83
|
+
path = described_class.ensure!(env: own, manifest: real, fetch: copy, warn: warner, platform: p)
|
|
84
|
+
expect(path).to eq(described_class.cached_path(env: own, manifest: real, platform: p))
|
|
85
|
+
expect(File.executable?(path)).to be(true)
|
|
86
|
+
end
|
|
87
|
+
expect(warnings).to be_empty
|
|
88
|
+
end
|
|
49
89
|
end
|
|
50
90
|
|
|
51
91
|
it 'derives the cache dir from RAIDER_CACHE_DIR or the home cache', :aggregate_failures do
|
|
@@ -254,6 +294,39 @@ describe RubyRaider::Ffmpeg do
|
|
|
254
294
|
expect(warnings).to eq(['cached ffmpeg failed checksum; video disabled'])
|
|
255
295
|
end
|
|
256
296
|
|
|
297
|
+
describe 'download size cap' do
|
|
298
|
+
let(:dest) { File.join(cache, 'download.part') }
|
|
299
|
+
let(:deadline) { Process.clock_gettime(Process::CLOCK_MONOTONIC) + 60 }
|
|
300
|
+
|
|
301
|
+
def ok_response(length, chunks)
|
|
302
|
+
response = Net::HTTPOK.new('1.1', '200', 'OK')
|
|
303
|
+
response['content-length'] = length.to_s if length
|
|
304
|
+
allow(response).to receive(:read_body) { |&block| chunks.each(&block) }
|
|
305
|
+
response
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'refuses a Content-Length over the cap before writing anything', :aggregate_failures do
|
|
309
|
+
response = ok_response(described_class::MAX_DOWNLOAD_BYTES + 1, ['x'])
|
|
310
|
+
expect { described_class.send(:handle_response, response, dest, deadline, 60) }
|
|
311
|
+
.to raise_error(IOError, /larger than/)
|
|
312
|
+
expect(File).not_to exist(dest)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'stops writing once the body passes the cap, whatever Content-Length said' do
|
|
316
|
+
stub_const("#{described_class}::MAX_DOWNLOAD_BYTES", 10)
|
|
317
|
+
response = ok_response(nil, ['x' * 6, 'x' * 6, 'x' * 6])
|
|
318
|
+
expect { described_class.send(:handle_response, response, dest, deadline, 60) }
|
|
319
|
+
.to raise_error(IOError, /larger than/)
|
|
320
|
+
expect(File.size(dest)).to be <= 10
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'writes a body within the cap' do
|
|
324
|
+
response = ok_response(4, %w[ab cd])
|
|
325
|
+
expect(described_class.send(:handle_response, response, dest, deadline, 60)).to be_nil
|
|
326
|
+
expect(File.read(dest)).to eq('abcd')
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
257
330
|
describe 'DEFAULT_WARN' do
|
|
258
331
|
it 'writes to $stderr even when $VERBOSE is nil' do
|
|
259
332
|
original = $VERBOSE
|
|
@@ -281,8 +354,8 @@ describe RubyRaider::Ffmpeg do
|
|
|
281
354
|
expect(described_class.obtainable?(env: {}, manifest:, platform: 'linux-x64')).to be(true)
|
|
282
355
|
end
|
|
283
356
|
|
|
284
|
-
it 'is false with no override and a nil checksum
|
|
285
|
-
expect(described_class.obtainable?(env: {}, platform: 'darwin-arm64')).to be(false)
|
|
357
|
+
it 'is false with no override and a nil checksum' do
|
|
358
|
+
expect(described_class.obtainable?(env: {}, manifest: unpublished, platform: 'darwin-arm64')).to be(false)
|
|
286
359
|
end
|
|
287
360
|
|
|
288
361
|
it 'is false for an unknown platform or a non-executable override' do
|
|
@@ -44,12 +44,25 @@ RSpec.describe UtilityCommands do
|
|
|
44
44
|
|
|
45
45
|
describe 'video install' do
|
|
46
46
|
it 'exits 1 and explains when the build is not published yet' do
|
|
47
|
+
require 'ruby_raider/ffmpeg'
|
|
48
|
+
unpublished = { version: '7.1', assets: { RubyRaider::Ffmpeg.platform.to_s => { url: 'https://example.test/x', sha256: nil } } }
|
|
49
|
+
stub_const('RubyRaider::Ffmpeg::MANIFEST', unpublished)
|
|
47
50
|
out, err, status = run_cli('video', 'install')
|
|
48
51
|
expect(status).to eq(1)
|
|
49
52
|
expect(err).to include('[video]', 'ffmpeg build not published yet')
|
|
50
53
|
expect(out).to eq('')
|
|
51
54
|
end
|
|
52
55
|
|
|
56
|
+
it 'exits 1 and installs nothing when the download does not match the pinned checksum', :aggregate_failures do
|
|
57
|
+
require 'ruby_raider/ffmpeg'
|
|
58
|
+
allow(RubyRaider::Ffmpeg).to receive(:http_get) { |_url, dest| File.write(dest, 'not ffmpeg') }
|
|
59
|
+
out, err, status = run_cli('video', 'install')
|
|
60
|
+
expect(status).to eq(1)
|
|
61
|
+
expect(err).to include('[video]', 'checksum mismatch')
|
|
62
|
+
expect(out).to eq('')
|
|
63
|
+
expect(Dir.glob(File.join(cache, '**', '*'))).to be_empty
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
it 'prints the path and exits 0 when ensure! succeeds' do
|
|
54
67
|
require 'ruby_raider/ffmpeg'
|
|
55
68
|
allow(RubyRaider::Ffmpeg).to receive(:ensure!).and_return('/tmp/ffmpeg')
|