test-recorder 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ce916b59fcd166bdcafb222263423739c9c8fe58e53a50f5895c95485145522
4
- data.tar.gz: 71fd0cb14988364e06d7977c38815a6656522c2acb79180ec06e5889b5eab155
3
+ metadata.gz: f76a3cec00d302949375e3ad8d9c4c566f8c52cac703db8384b36505d2be47bb
4
+ data.tar.gz: 109b533201bcde42f210224cf1a01f5b0b795cb1dc5c737d83a80a0b232d67b3
5
5
  SHA512:
6
- metadata.gz: 3caaba9397db23c5c2bdb215812345af54f04a21e25c56ae6c83209c45f42a2bfd1891a587e7c58682ee85bd07b806d077adb7f7e780e2cd1c202fa2a38e5b64
7
- data.tar.gz: 50f45dfc872ff6cc60a997f842bff3ba95d225cd88621879d2a07c0119c577376528ac3cedf306322c9c7efe1e18a1be63d232109c4d3ac29f3689d22aed27b9
6
+ metadata.gz: 5b4ca4f5087cc0d32cb335f79fdface896ae918493958ff753f36207f93e44b80a0d1af11f9486c8a6cdcad9d2dd2974599cabfac1afaf158e85d51842d5f71c
7
+ data.tar.gz: 52a41a7cc4a35ee019892cea4c67f467bb0e7a9e7f391bba7bfca52a596bda057b2fb92f72c9dc20ef85e700b52985dd049a2bfed6baac874cb3bad8479eb19d
@@ -3,32 +3,45 @@ name: CI
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - master
6
+ - main
7
7
 
8
8
  pull_request:
9
9
 
10
+ env:
11
+ FFMPEG_BUILD: ffmpeg-n8.1-latest-linux64-gpl-8.1
12
+
10
13
  jobs:
11
14
  build:
12
15
  runs-on: ubuntu-latest
16
+ timeout-minutes: 10
13
17
 
14
- concurrency:
15
- group: ${{ github.ref }}
16
- cancel-in-progress: true
18
+ strategy:
19
+ matrix:
20
+ ruby: [ '4.0', '3.4', '3.3' ]
17
21
 
18
22
  steps:
19
- - uses: actions/checkout@v3
23
+ - uses: actions/checkout@v4
20
24
  - uses: ruby/setup-ruby@v1
21
25
  with:
22
- ruby-version: '3.2'
23
- - uses: actions/setup-node@v2
26
+ ruby-version: ${{ matrix.ruby }}
27
+ bundler-cache: true
28
+ - name: Cache ffmpeg
29
+ id: cache-ffmpeg
30
+ uses: actions/cache@v4
24
31
  with:
25
- node-version: '18'
32
+ path: ~/.local/bin/ffmpeg
33
+ key: ffmpeg-${{ runner.os }}-${{ env.FFMPEG_BUILD }}
26
34
  - name: Install ffmpeg
35
+ if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
27
36
  run: |
28
- sudo apt update && sudo apt install ffmpeg
29
- - name: Install dependencies
37
+ mkdir -p "$HOME/.local/bin"
38
+ curl -fsSL -o "$RUNNER_TEMP/ffmpeg.tar.xz" \
39
+ "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/${FFMPEG_BUILD}.tar.xz"
40
+ tar -xJf "$RUNNER_TEMP/ffmpeg.tar.xz" -C "$RUNNER_TEMP" "${FFMPEG_BUILD}/bin/ffmpeg"
41
+ mv "$RUNNER_TEMP/${FFMPEG_BUILD}/bin/ffmpeg" "$HOME/.local/bin/ffmpeg"
42
+ - name: Set up ffmpeg
30
43
  run: |
31
- gem install bundler --no-document
32
- bundle install
44
+ chmod +x "$HOME/.local/bin/ffmpeg"
45
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
33
46
  - name: Run test
34
47
  run: bundle exec rake
@@ -0,0 +1,33 @@
1
+ name: Push Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - v*
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ push:
13
+ if: github.repository == 'y-yagi/test-recorder'
14
+ runs-on: ubuntu-latest
15
+
16
+ permissions:
17
+ contents: write
18
+ id-token: write
19
+
20
+ steps:
21
+ - name: Harden Runner
22
+ uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
23
+ with:
24
+ egress-policy: audit
25
+
26
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
27
+ - name: Set up Ruby
28
+ uses: ruby/setup-ruby@2e007403fc1ec238429ecaa57af6f22f019cc135 # v1.234.0
29
+ with:
30
+ bundler-cache: true
31
+ ruby-version: ruby
32
+
33
+ - uses: rubygems/release-gem@9e85cb11501bebc2ae661c1500176316d3987059 # v1.1.0
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## Unreleased
2
+
3
+ ## 0.3.0 - 2026-08-23
4
+
5
+ * Reduce recording overhead
6
+ * Make recording JPEG quality, max dimension and screencast frame interval configurable
7
+ * Explicitly encode videos with VP8 to avoid relying on ffmpeg's default codec
8
+ * Sanitize characters that can't use for video file names
9
+ * Stop recording a video for a skipped Rails system test
10
+
1
11
  ## 0.2.0 - 2023-08-23
2
12
 
3
13
  * Add support for aggregate_failures #21 (@willnet)
data/Gemfile CHANGED
@@ -3,6 +3,6 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in test-recorder.gemspec
4
4
  gemspec
5
5
 
6
- gem "rake", "~> 12.0"
6
+ gem "rake"
7
7
  gem "minitest", "~> 5.0"
8
8
  gem "debug"
data/README.md CHANGED
@@ -94,4 +94,21 @@ TestRecorder.disable!
94
94
  it "test to something", test_recorder: true do
95
95
  # ...
96
96
  end
97
+ ```
98
+
99
+ ### Configuration
100
+
101
+ You can change the recording quality and the max width/height (in pixels).
102
+
103
+ ```ruby
104
+ TestRecorder.jpeg_quality = 80 # default: 60
105
+ TestRecorder.max_dimension = 1280 # default: 1000
106
+ ```
107
+
108
+ You can also record only every Nth frame. This lowers the recording overhead
109
+ at the cost of a choppier video. The playback duration stays the same.
110
+
111
+ ```ruby
112
+ TestRecorder.every_nth_frame = 3 # default: 1
113
+ ```
97
114
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ Rake::TestTask.new(:test) do |t|
6
6
  t.libs << "lib"
7
7
  t.verbose = true
8
8
  t.warning = true
9
- t.test_files = ["test/integration_test.rb"]
9
+ t.test_files = ["test/integration_test.rb", "test/test_recorder_config_test.rb"]
10
10
  end
11
11
 
12
12
  task :default => :test
@@ -1,19 +1,49 @@
1
- require "open3"
1
+ require "base64"
2
2
  require "fileutils"
3
3
  require "tempfile"
4
4
 
5
5
  module TestRecorder
6
6
  class CdpRecorder
7
+ FFMPEG_ENCODE_OPTIONS = %w[-y -an -r 25 -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1].freeze
8
+
9
+ Record = Struct.new(:page, :io, :frame_count)
10
+
11
+ class << self
12
+ def record(devtools)
13
+ records[devtools] ||= begin
14
+ page = devtools.page
15
+ page.enable
16
+
17
+ record = Record.new(page, nil, 0)
18
+ page.on(:screencast_frame) do |event|
19
+ write_frame(record, event["data"])
20
+ record.page.screencast_frame_ack(session_id: event["sessionId"])
21
+ end
22
+ record
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def write_frame(record, data)
29
+ return if record.io.nil?
30
+
31
+ record.io.write(Base64.decode64(data))
32
+ record.frame_count += 1
33
+ rescue IOError
34
+ # A frame can still arrive after the recording was stopped and the file was closed.
35
+ rescue => e
36
+ warn "[TestRecorder] Failed to write a screencast frame: #{e.class}: #{e.message}"
37
+ end
38
+
39
+ def records
40
+ @records ||= {}.compare_by_identity
41
+ end
42
+ end
43
+
7
44
  def initialize(enabled:)
8
45
  @enabled = enabled
9
46
  @started = nil
10
- @page = nil
11
- setup
12
- end
13
-
14
- def setup
15
- @video_dir = ::Rails.root.join("tmp", "videos")
16
- FileUtils.mkdir_p(@video_dir)
17
47
  end
18
48
 
19
49
  def start(page:, enabled: nil)
@@ -21,40 +51,60 @@ module TestRecorder
21
51
  @started = enabled
22
52
  return unless @started
23
53
 
24
- @tmp_video = Tempfile.open(["testrecorder", ".webm"])
25
- cmd = "ffmpeg -loglevel quiet -f image2pipe -avioflags direct -fpsprobesize 0 -probesize 32 -analyzeduration 0 -c:v mjpeg -i - -y -an -r 25 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1 #{@tmp_video.path}"
26
- @stdin, @wait_thrs = *Open3.pipeline_w(cmd)
27
- @stdin.set_encoding("ASCII-8BIT")
54
+ @tmp_video = Tempfile.new(["testrecorder", ".mjpeg"])
55
+ @tmp_video.binmode
28
56
 
29
- @page = page
30
- @page.driver.browser.devtools.page.enable
57
+ @record = self.class.record(page.driver.browser.devtools)
58
+ @record.io = @tmp_video
59
+ @record.frame_count = 0
31
60
 
32
- @page.driver.browser.devtools.page.on(:screencast_frame) do |event|
33
- decoded_data = Base64.decode64(event["data"])
34
- @stdin.print(decoded_data) rescue nil
35
- @page.driver.browser.devtools.page.screencast_frame_ack(session_id: event["sessionId"])
36
- end
61
+ @every_nth_frame = TestRecorder.every_nth_frame
37
62
 
38
- @page.driver.browser.devtools.page.start_screencast(format: "jpeg", quality: 90)
63
+ @record.page.start_screencast(format: "jpeg", quality: TestRecorder.jpeg_quality, max_width: TestRecorder.max_dimension, max_height: TestRecorder.max_dimension, every_nth_frame: @every_nth_frame)
39
64
  end
40
65
 
41
66
  def stop_and_discard
42
- if @started
43
- @page.driver.browser.devtools.page.stop_screencast
44
- @stdin.close
45
- end
67
+ return unless @started
68
+
69
+ @record.io = nil
70
+ @record.page.stop_screencast
71
+ @tmp_video.close!
46
72
  end
47
73
 
48
74
  def stop_and_save(filename)
49
75
  return "" unless @started
50
76
 
51
- @page.driver.browser.devtools.page.stop_screencast
52
- @stdin.close
53
- @wait_thrs.each(&:join)
77
+ @record.io = nil
78
+ @record.page.stop_screencast
79
+ @tmp_video.flush
54
80
 
55
- video_path = File.join(@video_dir, filename)
56
- FileUtils.copy(@tmp_video.path, video_path)
57
- @tmp_video.close(true)
81
+ # Chrome sends a frame as soon as the screencast starts, so a test that fails
82
+ # before it draws anything still leaves behind the one frame of the blank page
83
+ # it started on.
84
+ if @record.frame_count < 2
85
+ @tmp_video.close!
86
+ return ""
87
+ end
88
+
89
+ video_dir = ::Rails.root.join("tmp", "videos")
90
+ FileUtils.mkdir_p(video_dir)
91
+ video_path = video_dir.join(filename).to_s
92
+
93
+ # Chrome captures at about 25 fps, but `every_nth_frame` makes it deliver only
94
+ # one out of every N frames. So the captured file holds 25 / N frames per second.
95
+ # Tell ffmpeg that input rate, otherwise it assumes 25 fps and the video plays
96
+ # N times faster than the actual test.
97
+ result = system("ffmpeg", "-loglevel", "error", "-f", "image2pipe", "-c:v", "mjpeg", "-framerate", "25/#{@every_nth_frame}", "-i", @tmp_video.path, *FFMPEG_ENCODE_OPTIONS, video_path)
98
+
99
+ @tmp_video.close!
100
+
101
+ if result.nil?
102
+ warn "[TestRecorder] Failed to execute ffmpeg. Please make sure that FFmpeg is installed."
103
+ return ""
104
+ elsif !result
105
+ warn "[TestRecorder] ffmpeg failed to encode #{video_path}."
106
+ return ""
107
+ end
58
108
 
59
109
  video_path
60
110
  end
@@ -10,11 +10,11 @@ module TestRecorder
10
10
  end
11
11
 
12
12
  def before_teardown
13
- if failures.empty?
14
- @cdp_recorder.stop_and_discard
15
- else
16
- video_path = @cdp_recorder.stop_and_save("failures_#{self.name}.webm")
13
+ if failures.any? { |failure| !failure.is_a?(Minitest::Skip) }
14
+ video_path = @cdp_recorder.stop_and_save("failures_#{TestRecorder.sanitize_filename(self.name)}.webm")
17
15
  puts "[Video]: #{video_path}" if File.exist?(video_path)
16
+ else
17
+ @cdp_recorder.stop_and_discard
18
18
  end
19
19
  ensure
20
20
  super
@@ -1,3 +1,4 @@
1
+ require "test_recorder"
1
2
  require "test_recorder/cdp_recorder"
2
3
  require "test_recorder/rails/setup_and_teardown"
3
4
 
@@ -1,10 +1,9 @@
1
+ require "test_recorder"
1
2
  require "test_recorder/cdp_recorder"
2
3
  require "test_recorder/rspec/example_wrapper"
3
4
 
4
5
  module TestRecorder
5
6
  module RSpec
6
- CHARS_TO_TRANSLATE = ['/', '.', ':', ',', "'", '"', " "].freeze
7
-
8
7
  class << self
9
8
  attr_accessor :cdp_recorder
10
9
 
@@ -20,7 +19,7 @@ module TestRecorder
20
19
  end
21
20
 
22
21
  def method_name(example)
23
- example.description.underscore.tr(CHARS_TO_TRANSLATE.join, "_")[0...200] + "_#{rand(1000)}"
22
+ TestRecorder.sanitize_filename(example.description.underscore)[0...200] + "_#{rand(1000)}"
24
23
  end
25
24
 
26
25
  def passed?(example)
@@ -1,3 +1,3 @@
1
1
  module TestRecorder
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/test_recorder.rb CHANGED
@@ -1,7 +1,15 @@
1
1
  require "test_recorder/version"
2
2
 
3
3
  module TestRecorder
4
+ DEFAULT_JPEG_QUALITY = 60
5
+ DEFAULT_MAX_DIMENSION = 1000
6
+ DEFAULT_EVERY_NTH_FRAME = 1
7
+
8
+ CHARS_TO_TRANSLATE = ['/', '.', ':', ',', "'", '"', " "].freeze
9
+
4
10
  class << self
11
+ attr_writer :jpeg_quality, :max_dimension, :every_nth_frame
12
+
5
13
  def enable!
6
14
  @enable = true
7
15
  end
@@ -13,5 +21,21 @@ module TestRecorder
13
21
  def enabled?
14
22
  defined?(@enable) ? @enable : true
15
23
  end
24
+
25
+ def jpeg_quality
26
+ defined?(@jpeg_quality) ? @jpeg_quality : DEFAULT_JPEG_QUALITY
27
+ end
28
+
29
+ def max_dimension
30
+ defined?(@max_dimension) ? @max_dimension : DEFAULT_MAX_DIMENSION
31
+ end
32
+
33
+ def every_nth_frame
34
+ defined?(@every_nth_frame) ? @every_nth_frame : DEFAULT_EVERY_NTH_FRAME
35
+ end
36
+
37
+ def sanitize_filename(name)
38
+ name.tr(CHARS_TO_TRANSLATE.join, "_")
39
+ end
16
40
  end
17
41
  end
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.summary = %q{Automatically record videos when tests failed.}
10
10
  spec.homepage = "http://github.com/y-yagi/test-recorder"
11
11
  spec.license = "MIT"
12
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
13
13
 
14
14
  spec.metadata["homepage_uri"] = spec.homepage
15
15
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-recorder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Yaginuma
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-08-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -66,7 +65,6 @@ dependencies:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '0'
69
- description:
70
68
  email:
71
69
  - yuuji.yaginuma@gmail.com
72
70
  executables: []
@@ -74,10 +72,10 @@ extensions: []
74
72
  extra_rdoc_files: []
75
73
  files:
76
74
  - ".github/workflows/ci.yml"
75
+ - ".github/workflows/push_gem.yml"
77
76
  - ".gitignore"
78
77
  - CHANGELOG.md
79
78
  - Gemfile
80
- - Gemfile.lock
81
79
  - LICENSE.txt
82
80
  - README.md
83
81
  - Rakefile
@@ -97,7 +95,6 @@ licenses:
97
95
  - MIT
98
96
  metadata:
99
97
  homepage_uri: http://github.com/y-yagi/test-recorder
100
- post_install_message:
101
98
  rdoc_options: []
102
99
  require_paths:
103
100
  - lib
@@ -105,15 +102,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
102
  requirements:
106
103
  - - ">="
107
104
  - !ruby/object:Gem::Version
108
- version: 2.3.0
105
+ version: 2.7.0
109
106
  required_rubygems_version: !ruby/object:Gem::Requirement
110
107
  requirements:
111
108
  - - ">="
112
109
  - !ruby/object:Gem::Version
113
110
  version: '0'
114
111
  requirements: []
115
- rubygems_version: 3.4.10
116
- signing_key:
112
+ rubygems_version: 3.6.7
117
113
  specification_version: 4
118
114
  summary: Automatically record videos when tests failed.
119
115
  test_files: []
data/Gemfile.lock DELETED
@@ -1,55 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- test-recorder (0.2.0)
5
- activesupport
6
- selenium-devtools
7
- selenium-webdriver (>= 4.0)
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- activesupport (7.0.7)
13
- concurrent-ruby (~> 1.0, >= 1.0.2)
14
- i18n (>= 1.6, < 2)
15
- minitest (>= 5.1)
16
- tzinfo (~> 2.0)
17
- activesupport-testing-metadata (0.1.0)
18
- activesupport (>= 4.0)
19
- concurrent-ruby (1.2.2)
20
- debug (1.8.0)
21
- irb (>= 1.5.0)
22
- reline (>= 0.3.1)
23
- i18n (1.14.1)
24
- concurrent-ruby (~> 1.0)
25
- io-console (0.6.0)
26
- irb (1.7.4)
27
- reline (>= 0.3.6)
28
- minitest (5.19.0)
29
- rake (12.3.3)
30
- reline (0.3.8)
31
- io-console (~> 0.5)
32
- rexml (3.2.6)
33
- rubyzip (2.3.2)
34
- selenium-devtools (0.115.0)
35
- selenium-webdriver (~> 4.2)
36
- selenium-webdriver (4.11.0)
37
- rexml (~> 3.2, >= 3.2.5)
38
- rubyzip (>= 1.2.2, < 3.0)
39
- websocket (~> 1.0)
40
- tzinfo (2.0.6)
41
- concurrent-ruby (~> 1.0)
42
- websocket (1.2.9)
43
-
44
- PLATFORMS
45
- ruby
46
-
47
- DEPENDENCIES
48
- activesupport-testing-metadata
49
- debug
50
- minitest (~> 5.0)
51
- rake (~> 12.0)
52
- test-recorder!
53
-
54
- BUNDLED WITH
55
- 2.4.3