bidi2pdf 0.1.13 → 0.1.15

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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +53 -0
  3. data/.devcontainer/devcontainer.json +51 -0
  4. data/.devcontainer/docker-compose.yml +33 -0
  5. data/.ruby-gemset +1 -1
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +93 -6
  8. data/README.md +80 -5
  9. data/docker/Dockerfile +1 -1
  10. data/docker/Dockerfile.slim +2 -2
  11. data/docker/chrome-driver/chrome.json +874 -0
  12. data/lib/bidi2pdf/bidi/browser_tab.rb +97 -4
  13. data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +178 -0
  14. data/lib/bidi2pdf/bidi/client.rb +12 -5
  15. data/lib/bidi2pdf/bidi/commands/base.rb +2 -0
  16. data/lib/bidi2pdf/bidi/commands/capture_screenshot.rb +33 -0
  17. data/lib/bidi2pdf/bidi/commands/set_viewport.rb +32 -0
  18. data/lib/bidi2pdf/bidi/commands.rb +2 -0
  19. data/lib/bidi2pdf/bidi/network_event.rb +12 -3
  20. data/lib/bidi2pdf/bidi/network_events.rb +9 -1
  21. data/lib/bidi2pdf/bidi/session.rb +1 -1
  22. data/lib/bidi2pdf/chromedriver_manager.rb +10 -6
  23. data/lib/bidi2pdf/cli.rb +11 -1
  24. data/lib/bidi2pdf/launcher.rb +5 -3
  25. data/lib/bidi2pdf/notifications/logging_subscriber.rb +2 -0
  26. data/lib/bidi2pdf/session_warmer.rb +377 -0
  27. data/lib/bidi2pdf/test_helpers/images.rb +8 -0
  28. data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +1 -2
  29. data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +1 -2
  30. data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +1 -1
  31. data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +22 -2
  32. data/lib/bidi2pdf/test_helpers/testcontainers/container_endpoint.rb +30 -0
  33. data/lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb +33 -0
  34. data/lib/bidi2pdf/test_helpers/testcontainers.rb +1 -0
  35. data/lib/bidi2pdf/version.rb +1 -1
  36. data/lib/bidi2pdf.rb +26 -1
  37. data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +16 -9
  38. data/sig/bidi2pdf/bidi/auth_interceptor.rbs +32 -7
  39. data/sig/bidi2pdf/bidi/browser.rbs +4 -31
  40. data/sig/bidi2pdf/bidi/browser_console_logger.rbs +59 -0
  41. data/sig/bidi2pdf/bidi/browser_tab.rbs +269 -25
  42. data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +81 -0
  43. data/sig/bidi2pdf/bidi/client.rbs +103 -43
  44. data/sig/bidi2pdf/bidi/command_manager.rbs +5 -17
  45. data/sig/bidi2pdf/bidi/commands/add_intercept.rbs +16 -8
  46. data/sig/bidi2pdf/bidi/commands/base.rbs +49 -9
  47. data/sig/bidi2pdf/bidi/commands/browser_close.rbs +3 -1
  48. data/sig/bidi2pdf/bidi/commands/browser_create_user_context.rbs +2 -0
  49. data/sig/bidi2pdf/bidi/commands/browser_remove_user_context.rbs +19 -0
  50. data/sig/bidi2pdf/bidi/commands/browsing_context_close.rbs +8 -2
  51. data/sig/bidi2pdf/bidi/commands/browsing_context_navigate.rbs +12 -6
  52. data/sig/bidi2pdf/bidi/commands/browsing_context_print.rbs +10 -5
  53. data/sig/bidi2pdf/bidi/commands/cancel_auth.rbs +8 -2
  54. data/sig/bidi2pdf/bidi/commands/capture_screenshot.rbs +31 -0
  55. data/sig/bidi2pdf/bidi/commands/cdp_get_session.rbs +17 -0
  56. data/sig/bidi2pdf/bidi/commands/create_window.rbs +4 -0
  57. data/sig/bidi2pdf/bidi/commands/get_user_contexts.rbs +2 -0
  58. data/sig/bidi2pdf/bidi/commands/network_continue.rbs +4 -0
  59. data/sig/bidi2pdf/bidi/commands/page_print.rbs +28 -0
  60. data/sig/bidi2pdf/bidi/commands/print_parameters_validator.rbs +3 -0
  61. data/sig/bidi2pdf/bidi/commands/provide_credentials.rbs +12 -6
  62. data/sig/bidi2pdf/bidi/commands/script_evaluate.rbs +4 -0
  63. data/sig/bidi2pdf/bidi/commands/session_end.rbs +2 -0
  64. data/sig/bidi2pdf/bidi/commands/session_status.rbs +3 -1
  65. data/sig/bidi2pdf/bidi/commands/session_subscribe.rbs +4 -0
  66. data/sig/bidi2pdf/bidi/commands/set_tab_cookie.rbs +51 -23
  67. data/sig/bidi2pdf/bidi/commands/set_usercontext_cookie.rbs +17 -19
  68. data/sig/bidi2pdf/bidi/commands/set_viewport.rbs +31 -0
  69. data/sig/bidi2pdf/bidi/connection_manager.rbs +2 -1
  70. data/sig/bidi2pdf/bidi/event_manager.rbs +14 -19
  71. data/sig/bidi2pdf/bidi/interceptor.rbs +5 -0
  72. data/sig/bidi2pdf/bidi/js_logger_helper.rbs +9 -0
  73. data/sig/bidi2pdf/bidi/logger_events.rbs +21 -0
  74. data/sig/bidi2pdf/bidi/navigation_failed_events.rbs +17 -0
  75. data/sig/bidi2pdf/bidi/network_event.rbs +64 -46
  76. data/sig/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rbs +63 -0
  77. data/sig/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rbs +15 -0
  78. data/sig/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rbs +18 -0
  79. data/sig/bidi2pdf/bidi/network_event_formatters.rbs +6 -0
  80. data/sig/bidi2pdf/bidi/network_events.rbs +14 -39
  81. data/sig/bidi2pdf/bidi/session.rbs +144 -31
  82. data/sig/bidi2pdf/bidi/user_context.rbs +64 -45
  83. data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +22 -30
  84. data/sig/bidi2pdf/chromedriver_manager.rbs +61 -22
  85. data/sig/bidi2pdf/cli.rbs +22 -2
  86. data/sig/bidi2pdf/dsl.rbs +34 -0
  87. data/sig/bidi2pdf/launcher.rbs +39 -1
  88. data/sig/bidi2pdf/notifications/event.rbs +40 -0
  89. data/sig/bidi2pdf/notifications/instrumenter.rbs +21 -0
  90. data/sig/bidi2pdf/notifications/logging_subscriber.rbs +41 -0
  91. data/sig/bidi2pdf/notifications.rbs +24 -0
  92. data/sig/bidi2pdf/process_tree.rbs +2 -2
  93. data/sig/bidi2pdf/session_runner.rbs +41 -1
  94. data/sig/bidi2pdf/session_warmer.rbs +221 -0
  95. data/sig/bidi2pdf/test_helpers/configuration.rbs +51 -0
  96. data/sig/bidi2pdf/test_helpers/images/extractor.rbs +43 -0
  97. data/sig/bidi2pdf/test_helpers/images/image_similarity_checker.rbs +35 -0
  98. data/sig/bidi2pdf/test_helpers/images/tiff_helper.rbs +70 -0
  99. data/sig/bidi2pdf/test_helpers/images.rbs +0 -0
  100. data/sig/bidi2pdf/test_helpers/matchers/contains_pdf_image.rbs +0 -0
  101. data/sig/bidi2pdf/test_helpers/matchers/contains_pdf_text.rbs +0 -0
  102. data/sig/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rbs +0 -0
  103. data/sig/bidi2pdf/test_helpers/matchers/match_pdf_text.rbs +0 -0
  104. data/sig/bidi2pdf/test_helpers/pdf_file_helper.rbs +23 -0
  105. data/sig/bidi2pdf/test_helpers/pdf_reader_utils.rbs +49 -0
  106. data/sig/bidi2pdf/test_helpers/pdf_text_sanitizer.rbs +38 -0
  107. data/sig/bidi2pdf/test_helpers/spec_paths_helper.rbs +35 -0
  108. data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rbs +43 -0
  109. data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rbs +45 -0
  110. data/sig/bidi2pdf/test_helpers/testcontainers/container_endpoint.rbs +25 -0
  111. data/sig/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rbs +3 -0
  112. data/sig/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rbs +47 -0
  113. data/sig/bidi2pdf/test_helpers/testcontainers.rbs +6 -0
  114. data/sig/bidi2pdf/test_helpers.rbs +0 -0
  115. data/sig/bidi2pdf/verbose_logger.rbs +37 -0
  116. data/sig/bidi2pdf/version.rbs +3 -0
  117. data/sig/bidi2pdf.rbs +130 -2
  118. data/tasks/generate_rbs.rake +11 -11
  119. data/tasks/release_credentials_check.rake +97 -0
  120. metadata +67 -10
  121. data/sig/bidi2pdf/utils.rbs +0 -5
@@ -0,0 +1,21 @@
1
+ module Bidi2pdf
2
+ # rubocop: disable Lint/RescueException, Lint/SuppressedException
3
+ module Notifications
4
+ class Instrumenter
5
+ @id: untyped
6
+
7
+ attr_reader id: untyped
8
+
9
+ def initialize: () -> void
10
+
11
+ def notify: (untyped name, untyped payload) { (?) -> untyped } -> untyped
12
+
13
+ private
14
+
15
+ def create_event: (untyped name, untyped payload) -> untyped
16
+
17
+ # rubocop:disable Style/CaseEquality
18
+ def notify_subscribers: (untyped name, untyped event) -> untyped
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Bidi2pdf
2
+ module Notifications
3
+ # for reuse within ActiveSupport::LogSubscriber
4
+ module LoggingSubscriberActions
5
+ def handle_response: (untyped event) -> untyped
6
+
7
+ def send_cmd: (untyped event) -> untyped
8
+
9
+ def send_cmd_and_wait: (untyped event) -> (nil | untyped)
10
+
11
+ def session_close: (untyped event) -> (nil | untyped)
12
+
13
+ # rubocop: disable Metrics/AbcSize
14
+ def network_event_received: (untyped event) -> (nil | untyped)
15
+
16
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
17
+ def network_idle: (untyped event) -> (nil | untyped)
18
+
19
+ def page_loaded: (untyped event) -> untyped
20
+
21
+ def print: (untyped event) -> untyped
22
+
23
+ private
24
+
25
+ def redact_sensitive_fields: (untyped obj, ?::Array[untyped] sensitive_keys) -> untyped
26
+ end
27
+
28
+ class LoggingSubscriber
29
+ @logger: untyped
30
+
31
+ include LoggingSubscriberActions
32
+
33
+ attr_accessor logger: untyped
34
+
35
+ # rubocop: disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
36
+ def initialize: (?logger: untyped) -> void
37
+
38
+ def unsubscribe: () -> untyped
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ module Bidi2pdf
2
+ module Notifications
3
+ self.@subscribers: untyped
4
+
5
+ attr_reader self.subscribers: untyped
6
+
7
+ def self.instrument: (untyped name, ?::Hash[untyped, untyped] payload) ?{ (untyped) -> untyped } -> untyped
8
+
9
+ def self.subscribe: (untyped event_pattern) { (?) -> untyped } -> untyped
10
+
11
+ def self.unsubscribe: (untyped event_pattern, ?untyped? block) -> untyped
12
+
13
+ # rubocop: disable Style/CaseEquality
14
+ def self.listening?: (untyped name) -> untyped
15
+
16
+ private
17
+
18
+ def self.bidi2pdf_notification_instrumenter: () -> untyped
19
+
20
+ def self.notify: (untyped name, untyped payload) { (?) -> untyped } -> untyped
21
+
22
+ def self.normalize_pattern: (untyped pat) -> untyped
23
+ end
24
+ end
@@ -10,7 +10,7 @@ module Bidi2pdf
10
10
 
11
11
  def children: (untyped of_pid) -> (::Array[untyped] | untyped)
12
12
 
13
- def traverse: () { () -> untyped } -> untyped
13
+ def traverse: () { (?) -> untyped } -> untyped
14
14
 
15
15
  private
16
16
 
@@ -22,6 +22,6 @@ module Bidi2pdf
22
22
 
23
23
  def root_pids: () -> (::Array[untyped] | untyped)
24
24
 
25
- def traverse_branch: (untyped pid, ?::Integer level) { () -> untyped } -> (nil | untyped)
25
+ def traverse_branch: (untyped pid, ?::Integer level) { (?) -> untyped } -> (nil | untyped)
26
26
  end
27
27
  end
@@ -1,9 +1,43 @@
1
1
  module Bidi2pdf
2
+ # Represents a runner for managing browser sessions and executing tasks
3
+ # using the Bidi2pdf library. This class handles the setup, configuration,
4
+ # and execution of browser-related workflows, including navigation, cookie
5
+ # management, and printing.
6
+ #
7
+ # @example Running a session
8
+ # session_runner = Bidi2pdf::SessionRunner.new(
9
+ # session: session,
10
+ # url: "http://example.com",
11
+ # inputfile: "input.html",
12
+ # output: "output.pdf",
13
+ # cookies: { "key" => "value" },
14
+ # headers: { "Authorization" => "Bearer token" },
15
+ # auth: { username: "user", password: "pass" },
16
+ # wait_window_loaded: true,
17
+ # wait_network_idle: true,
18
+ # print_options: { landscape: true },
19
+ # network_log_format: :json
20
+ # )
21
+ # session_runner.run
22
+ #
23
+ # @param [Object] session The browser session object to use.
24
+ # @param [String, nil] url The URL to navigate to in the browser session.
25
+ # @param [String, nil] inputfile The path to the input file to be processed if no URL is provided.
26
+ # @param [String, nil] output The path to the output file to be generated.
27
+ # @param [Hash] cookies A hash of cookies to set in the browser session. Defaults to an empty hash.
28
+ # @param [Hash] headers A hash of HTTP headers to include in the browser session. Defaults to an empty hash.
29
+ # @param [Hash, nil] auth Authentication credentials (e.g., username and password). Defaults to an empty hash.
30
+ # @param [Boolean] wait_window_loaded Whether to wait for the window to fully load. Defaults to false.
31
+ # @param [Boolean] wait_network_idle Whether to wait for the network to become idle. Defaults to false.
32
+ # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
33
+ # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
2
34
  class SessionRunner
3
35
  @session: untyped
4
36
 
5
37
  @url: untyped
6
38
 
39
+ @inputfile: untyped
40
+
7
41
  @output: untyped
8
42
 
9
43
  @cookies: untyped
@@ -18,13 +52,18 @@ module Bidi2pdf
18
52
 
19
53
  @print_options: untyped
20
54
 
55
+ @network_log_format: untyped
56
+
21
57
  @window: untyped
22
58
 
23
59
  @tab: untyped
24
60
 
61
+ @user_context: untyped
62
+
25
63
  @uri: untyped
26
64
 
27
- def initialize: (session: untyped, url: untyped, output: untyped, ?cookies: ::Hash[untyped, untyped], ?headers: ::Hash[untyped, untyped], ?auth: ::Hash[untyped, untyped], ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped]) -> void
65
+ # rubocop: disable Metrics/ParameterLists
66
+ def initialize: (session: untyped, url: untyped, inputfile: untyped, output: untyped, ?cookies: ::Hash[untyped, untyped], ?headers: ::Hash[untyped, untyped], ?auth: ::Hash[untyped, untyped], ?wait_window_loaded: bool, ?wait_network_idle: bool, ?print_options: ::Hash[untyped, untyped], ?network_log_format: ::Symbol) -> void
28
67
 
29
68
  def run: () -> untyped
30
69
 
@@ -38,6 +77,7 @@ module Bidi2pdf
38
77
 
39
78
  def add_basic_auth: () -> (nil | untyped)
40
79
 
80
+ # rubocop: disable Metrics/AbcSize
41
81
  def run_flow: () -> untyped
42
82
 
43
83
  def uri: () -> untyped
@@ -0,0 +1,221 @@
1
+ module Bidi2pdf
2
+ # Keeps a small number of Chrome sessions pre-warmed (chromedriver started, WebSocket connected,
3
+ # browser ready) so a render can skip that startup latency on the request path. Isolation matches
4
+ # today's one-Chrome-per-render model exactly: every checked-out slot is used for exactly one
5
+ # +with_tab+ block and then retired (never returned to the cache) - only a fresh replacement is
6
+ # warmed in its place, off the request path. Checkout never waits for a warm slot: one is used if
7
+ # it is ready, otherwise a slot is created synchronously on the spot, i.e. today's exact behavior
8
+ # for that one render - including its failure mode: if that cold start fails, the error propagates
9
+ # out of +with_tab+ exactly as it would without the warmer.
10
+ #
11
+ # @example Rails initializer
12
+ # Bidi2pdf::SessionWarmer.configure do |c|
13
+ # c.size = 2
14
+ # c.headless = true
15
+ # end
16
+ #
17
+ # @example Per-request usage
18
+ # Bidi2pdf::SessionWarmer.with_tab do |tab|
19
+ # tab.navigate_to(url)
20
+ # tab.print("invoice.pdf")
21
+ # end
22
+ class SessionWarmer
23
+ self.@config: untyped
24
+
25
+ # Cleared first: if the constructor below raises, a stale, already-shut-down instance must
26
+ # not stay registered (it would keep serving cold slots but never warm again).
27
+ self.@instance: untyped
28
+
29
+ @config: untyped
30
+
31
+ @slot_factory: untyped
32
+
33
+ @mutex: untyped
34
+
35
+ @available: untyped
36
+
37
+ @replenish_threads: untyped
38
+
39
+ @warming: untyped
40
+
41
+ @shutdown: untyped
42
+
43
+ @reaper_wakeup: untyped
44
+
45
+ @reaper: untyped
46
+
47
+ # Configuration for the session warmer.
48
+ class Configuration
49
+ @size: untyped
50
+
51
+ @headless: untyped
52
+
53
+ @chrome_args: untyped
54
+
55
+ @remote_browser_url: untyped
56
+
57
+ @slot_factory: untyped
58
+
59
+ @max_idle_age: untyped
60
+
61
+ # @return [Integer] Number of Chrome slots to keep pre-warmed.
62
+ attr_accessor size: untyped
63
+
64
+ # @return [Boolean] Whether to run Chrome in headless mode.
65
+ attr_accessor headless: untyped
66
+
67
+ # @return [Array<String>] Chrome launch arguments.
68
+ attr_accessor chrome_args: untyped
69
+
70
+ # @return [String, nil] A remote chromedriver session URL (e.g. a `remote-chrome` sidecar). When
71
+ # set, a slot connects directly to it instead of spawning a local ChromedriverManager -
72
+ # mirrors Launcher#session's own local/remote branch.
73
+ attr_accessor remote_browser_url: untyped
74
+
75
+ # @return [#call, nil] Optional factory callable that returns a slot hash - injectable for tests.
76
+ # A custom factory owns the cleanup of anything it half-built before raising: the warmer can
77
+ # only retire a slot it was actually handed.
78
+ attr_accessor slot_factory: untyped
79
+
80
+ # @return [Numeric, nil] Seconds a warm slot may sit unused before it is retired and replaced.
81
+ # A warm slot is an open, unauthenticated automation endpoint (chromedriver's port, Chrome's
82
+ # debugging port) for as long as it idles, so that window is bounded by default. A slot older
83
+ # than this is never handed out, and a background reaper recycles idle ones within roughly
84
+ # 1.25x this value even when no render ever comes. +nil+ disables the limit.
85
+ attr_accessor max_idle_age: untyped
86
+
87
+ DEFAULT_MAX_IDLE_AGE: 300
88
+
89
+ def initialize: () -> void
90
+
91
+ # @raise [ArgumentError] if a setting can't be honored - checked once, at construction.
92
+ def validate!: () -> untyped
93
+
94
+ private
95
+
96
+ def validate_size!: () -> (nil | untyped)
97
+
98
+ def validate_max_idle_age!: () -> (nil | untyped)
99
+ end
100
+
101
+ # Configures the warmer and eagerly (re)creates the singleton, warming config.size slots right
102
+ # here - at boot/configuration time, off the request path - instead of lazily on whichever
103
+ # request happens to trigger the first #with_tab (which would otherwise pay for every
104
+ # configured slot, serially, on that one unlucky request).
105
+ def self.configure: () ?{ (untyped) -> untyped } -> untyped
106
+
107
+ # Returns the warmer's configuration, initializing defaults if needed.
108
+ def self.config: () -> untyped
109
+
110
+ # Returns a callable that creates one real, fully-warmed Chrome slot from +config+. If building
111
+ # fails part-way (chromedriver up, but the session or its browser never came ready), whatever
112
+ # already exists is retired before the error propagates - no caller ever gets a reference to a
113
+ # half-built slot, so nobody else could clean it up.
114
+ def self.default_slot_factory: (untyped config) -> untyped
115
+
116
+ # Closes a slot's session and stops its chromedriver (nil for a remote slot, or for a part that
117
+ # was never created). Each step is independent: a failure in one is logged, never raised, and
118
+ # never skips the other.
119
+ def self.retire_slot: (session: untyped, manager: untyped) -> untyped
120
+
121
+ def self.safe_close: (untyped label) { () -> untyped } -> untyped
122
+
123
+ # Returns the shared singleton warmer instance, creating it (and pre-warming it) on first call.
124
+ def self.instance: () -> untyped
125
+
126
+ # Checks out a slot, yields a fresh tab for one render, then retires the slot.
127
+ def self.with_tab: () { (?) -> untyped } -> untyped
128
+
129
+ # Retires every currently-warm spare and resets the singleton.
130
+ def self.shutdown: () -> untyped
131
+
132
+ private
133
+
134
+ # Records each part in +parts+ the moment it exists, so #default_slot_factory's rescue can
135
+ # retire exactly what was created so far. Mirrors Launcher#session's local/remote branch.
136
+ def self.build_slot: (untyped config, untyped parts) -> { session: untyped, browser: untyped, manager: untyped }
137
+
138
+ def self.connect_remote: (untyped config, untyped parts) -> untyped
139
+
140
+ def self.start_local: (untyped config, untyped parts) -> untyped
141
+
142
+ public
143
+
144
+ def initialize: (untyped config, ?slot_factory: untyped?) -> void
145
+
146
+ # Checks out a slot, creates an isolated UserContext/Window/Tab for one render, yields the tab,
147
+ # then unconditionally closes those resources and retires the underlying slot.
148
+ def with_tab: () { (untyped) -> untyped } -> untyped
149
+
150
+ # Retires every currently-warm spare and waits for any in-flight background replenishment to
151
+ # finish (each of those, seeing @shutdown, retires its own result instead of stashing it - see
152
+ # #stash_or_retire). A subsequent #with_tab still works - it just falls back to a synchronous
153
+ # slot, since checkout never depends on a warm one being there.
154
+ def shutdown: () -> untyped
155
+
156
+ private
157
+
158
+ # Fail-fast on purpose (a Chrome that can't start at boot should be loud), but not leaky: if slot
159
+ # N fails, no instance is returned to own slots 1..N-1, so they are retired here first.
160
+ def prewarm: () -> untyped
161
+
162
+ def create_slot: () -> untyped
163
+
164
+ # Session#started? is just an internal flag set once at startup - it never flips back if Chrome,
165
+ # ChromeDriver, or the WebSocket dies externally while a slot sits idle in the cache. The
166
+ # client's own #open? is kept live by the reader thread noticing a real socket error, so
167
+ # checking it too catches that case - cheap (no network round trip), though still a heuristic,
168
+ # not a full liveness guarantee (a stuck-but-not-yet-disconnected socket still reads healthy).
169
+ def healthy?: (untyped slot) -> untyped
170
+
171
+ def now: () -> untyped
172
+
173
+ # Marks the moment a slot entered the cache - idle age counts from here, not from when its
174
+ # Chrome started, since it is the unattended waiting that max_idle_age bounds.
175
+ def stamp: (untyped slot) -> untyped
176
+
177
+ def expired?: (untyped slot) -> untyped
178
+
179
+ # Checkout alone can't bound idle time: with no traffic nothing would ever look at a spare. This
180
+ # thread does, four times per max_idle_age, so an unused slot is recycled within ~1.25x of it.
181
+ # Thread::Queue#pop(timeout:) doubles as an interruptible sleep - #shutdown pushes :stop.
182
+ def reap_loop: () -> untyped
183
+
184
+ def recycle_expired: () -> untyped
185
+
186
+ # A warm hit takes the spare and triggers a background replacement; a miss (empty cache, or a
187
+ # spare that died while idle) falls straight through to a synchronous slot - it never waits, so
188
+ # an under-provisioned warmer is never worse than not having one. A cold start that fails raises,
189
+ # as it would without the warmer.
190
+ #
191
+ # Oldest spare first (shift, not pop), so no slot lingers at the bottom of the cache while newer
192
+ # ones are used; and one past max_idle_age is never handed out, even if the reaper hasn't got to
193
+ # it yet - it is retired like a dead spare and this render cold-starts instead.
194
+ def checkout: () -> untyped
195
+
196
+ def cold_checkout: (untyped dead_slot) -> untyped
197
+
198
+ # Tops the cache up towards config.size, counting warmers already in flight. Deficit-based on
199
+ # purpose, not "replace what this checkout popped": that rule could never recover from a single
200
+ # failed warm (nothing stashed -> every later checkout a miss -> never replenished again), while
201
+ # replenishing unconditionally let @available grow without bound under a burst of misses.
202
+ # available + warming never exceeds config.size, and a failed warm frees its reservation, so the
203
+ # next checkout simply tries again.
204
+ #
205
+ # Threads are created and registered inside the same critical section, so #shutdown's snapshot
206
+ # can't miss one that has started but isn't listed yet (#warm_one needs this mutex to finish,
207
+ # so it just waits for it).
208
+ def replenish_async: () -> untyped
209
+
210
+ def warm_one: () -> untyped
211
+
212
+ # A replacement warmed after #shutdown has nothing to stash into - retire it immediately rather
213
+ # than leaking a live Chrome process that nothing will ever check out. Either way this warmer's
214
+ # reservation is released here, in the same critical section as the stash.
215
+ def stash_or_retire: (untyped slot) -> untyped
216
+
217
+ def retire: (untyped slot) -> untyped
218
+
219
+ def safe_close: (untyped label) { (?) -> untyped } -> untyped
220
+ end
221
+ end
@@ -0,0 +1,51 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ self.@configuration: untyped
4
+
5
+ class Configuration
6
+ @spec_dir: untyped
7
+
8
+ @docker_dir: untyped
9
+
10
+ @fixture_dir: untyped
11
+
12
+ @tmp_dir: untyped
13
+
14
+ @prefix: untyped
15
+
16
+ # @!attribute [rw] spec_dir
17
+ # @return [Pathname] the directory where specs are located
18
+ attr_accessor spec_dir: untyped
19
+
20
+ # @!attribute [rw] tmp_dir
21
+ # @return [String] the directory for temporary files
22
+ attr_accessor tmp_dir: untyped
23
+
24
+ # @!attribute [rw] prefix
25
+ # @return [String] the prefix for temporary files
26
+ attr_accessor prefix: untyped
27
+
28
+ # @!attribute [rw] docker_dir
29
+ # @return [String] the directory for Docker files
30
+ attr_accessor docker_dir: untyped
31
+
32
+ # @!attribute [rw] fixture_dir
33
+ # @return [String] the directory for fixture files
34
+ attr_accessor fixture_dir: untyped
35
+
36
+ def initialize: () -> void
37
+ end
38
+
39
+ # Retrieves the current configuration object for TestHelpers.
40
+ # @return [Configuration] the configuration object
41
+ def self.configuration: () -> untyped
42
+
43
+ # Sets the configuration object for TestHelpers.
44
+ # @param [Configuration] config the configuration object to set
45
+ attr_writer self.configuration: untyped
46
+
47
+ # Allows configuration of TestHelpers by yielding the configuration object.
48
+ # @yieldparam [Configuration] configuration the configuration object to modify
49
+ def self.configure: () { (untyped) -> untyped } -> untyped
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ module Images
4
+ class Extractor
5
+ @pages: untyped
6
+
7
+ @logger: untyped
8
+
9
+ @extracted_images: untyped
10
+
11
+ include PDFReaderUtils
12
+
13
+ include TIFFHelper
14
+
15
+ attr_reader pages: untyped
16
+
17
+ attr_reader logger: untyped
18
+
19
+ def initialize: (untyped pdf_data, ?logger: untyped) -> void
20
+
21
+ def all_images: () -> untyped
22
+
23
+ def image_on_page: (untyped page_number, untyped image_number) -> (nil | untyped)
24
+
25
+ def images_on_page: (untyped page_number) -> untyped
26
+
27
+ private
28
+
29
+ def extracted_images: () -> untyped
30
+
31
+ def extract_images: (untyped page) -> (nil | untyped)
32
+
33
+ def process_image_stream: (untyped stream) -> (nil | untyped)
34
+
35
+ def extract_raw_image_data: (untyped stream, untyped filter) -> untyped
36
+
37
+ def handle_compressed_image: (untyped stream) -> untyped
38
+
39
+ def create_vips_image: (untyped raw, untyped filter) -> untyped
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ module Images
4
+ class ImageSimilarityChecker
5
+ @expected_image: untyped
6
+
7
+ @image_to_check: untyped
8
+
9
+ @expected_fingerprint: untyped
10
+
11
+ @actual_fingerprint: untyped
12
+
13
+ @distance: untyped
14
+
15
+ def initialize: (untyped expected_image, untyped image_to_check) -> void
16
+
17
+ def similar?: (?tolerance: ::Integer) -> untyped
18
+
19
+ def very_similar?: () -> untyped
20
+
21
+ def slightly_similar?: () -> untyped
22
+
23
+ def different?: () -> untyped
24
+
25
+ def expected_fingerprint: () -> untyped
26
+
27
+ def actual_fingerprint: () -> untyped
28
+
29
+ def distance: () -> untyped
30
+
31
+ def fingerprint: (untyped image) -> untyped
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,70 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ module Images
4
+ # rubocop: disable Metrics/ModuleLength, Metrics/AbcSize
5
+ module TIFFHelper
6
+ # TIFF Tag IDs
7
+ IMAGE_WIDTH: 256
8
+
9
+ IMAGE_LENGTH: 257
10
+
11
+ BITS_PER_SAMPLE: 258
12
+
13
+ COMPRESSION: 259
14
+
15
+ PHOTOMETRIC_INTERPRETATION: 262
16
+
17
+ STRIP_OFFSETS: 273
18
+
19
+ SAMPLES_PER_PIXEL: 277
20
+
21
+ ROWS_PER_STRIP: 278
22
+
23
+ STRIP_BYTE_COUNTS: 279
24
+
25
+ PLANAR_CONFIGURATION: 284
26
+
27
+ INK_SET: 332
28
+
29
+ # TIFF Data Types
30
+ TYPE_SHORT: 3
31
+
32
+ TYPE_LONG: 4
33
+
34
+ # TIFF Compression Types
35
+ COMPRESSION_NONE: 1
36
+
37
+ COMPRESSION_CCITT_G3: 3
38
+
39
+ COMPRESSION_CCITT_G4: 4
40
+
41
+ # TIFF Photometric Interpretations
42
+ PHOTO_WHITE_IS_ZERO: 0
43
+
44
+ PHOTO_BLACK_IS_ZERO: 1
45
+
46
+ PHOTO_RGB: 2
47
+
48
+ PHOTO_SEPARATION: 5
49
+
50
+ # Planar Configuration
51
+ PLANAR_CHUNKY: 1
52
+
53
+ def tiff_header: (untyped hash, untyped data) -> untyped
54
+
55
+ # See:
56
+ # * https://gist.github.com/gstorer/f6a9f1dfe41e8e64dcf58d07afa9ab2a
57
+ # * https://github.com/yob/pdf-reader/blob/main/examples/extract_images.rb
58
+ def pack_tiff: (untyped entries) -> untyped
59
+
60
+ def tiff_header_for_gray: (untyped hash, untyped data) -> untyped
61
+
62
+ def tiff_header_for_ccitt: (untyped hash, untyped data) -> untyped
63
+
64
+ def tiff_header_for_cmyk: (untyped hash, untyped data) -> untyped
65
+
66
+ def tiff_header_for_rgb: (untyped hash, untyped data) -> untyped
67
+ end
68
+ end
69
+ end
70
+ end
File without changes
@@ -0,0 +1,23 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ # This module provides helper methods for handling PDF files in tests.
4
+ # It includes methods for debugging, storing, and managing PDF files.
5
+ module PdfFileHelper
6
+ def reporter: () -> untyped
7
+
8
+ # Executes a block with the given PDF data and handles debugging in case of test failures.
9
+ # If an expectation fails, the PDF data is saved to a file for debugging purposes.
10
+ # @param [String] pdf_data the PDF data to debug
11
+ # @yield [String] yields the PDF data to the given block
12
+ # @raise [RSpec::Expectations::ExpectationNotMetError] re-raises the exception after saving the PDF
13
+ def with_pdf_debug: (untyped pdf_data) { (untyped) -> untyped } -> untyped
14
+
15
+ # Stores the given PDF data to a file with a specified filename prefix.
16
+ # The file is saved in a temporary directory.
17
+ # @param [String] pdf_data the PDF data to store
18
+ # @param [String] filename_prefix the prefix for the generated filename (default: "test")
19
+ # @return [String] the full path to the saved PDF file
20
+ def store_pdf_file: (untyped pdf_data, ?::String filename_prefix) -> untyped
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Bidi2pdf
2
+ module TestHelpers
3
+ module PDFReaderUtils
4
+ # Extracts text content from a PDF document.
5
+ #
6
+ # This method accepts various PDF input formats and attempts to extract text content
7
+ # from all pages. If extraction fails due to malformed PDF data, it returns the original input.
8
+ #
9
+ # @param pdf_data [String, StringIO, File] The PDF data in one of the following formats:
10
+ # * Base64-encoded PDF string
11
+ # * Raw PDF data beginning with "%PDF-"
12
+ # * StringIO object containing PDF data
13
+ # * Path to a PDF file as String
14
+ # * Raw PDF data as String
15
+ # @return [Array<String>] An array of strings, with each string representing the text content of a page
16
+ # @return [Object] The original input if PDF extraction fails
17
+ # @example Extract text from a PDF file
18
+ # text_content = pdf_text('path/to/document.pdf')
19
+ #
20
+ # @example Extract text from Base64-encoded string
21
+ # text_content = pdf_text(base64_encoded_pdf_data)
22
+ def self.pdf_text: (untyped pdf_data) -> untyped
23
+
24
+ # Converts the input PDF data into an IO object and initializes a PDF::Reader.
25
+ #
26
+ # @param pdf_data [String, StringIO, File] The PDF data to be read.
27
+ # @return [PDF::Reader] A PDF::Reader instance for the given data.
28
+ # @raise [PDF::Reader::MalformedPDFError] If the PDF data is invalid.
29
+ def self.pdf_reader_for: (untyped pdf_data) -> untyped
30
+
31
+ # rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
32
+ # Converts various input formats into an IO object for PDF::Reader.
33
+ #
34
+ # @param pdf_data [String, StringIO, File] The PDF data to be converted.
35
+ # @return [IO] An IO object containing the PDF data.
36
+ def self.convert_data_to_io: (untyped pdf_data) -> untyped
37
+
38
+ module InstanceMethods
39
+ def pdf_text: (untyped pdf_data) -> untyped
40
+
41
+ def pdf_reader_for: (untyped pdf_data) -> untyped
42
+
43
+ def convert_data_to_io: (untyped pdf_data) -> untyped
44
+ end
45
+
46
+ def self.included: (untyped base) -> untyped
47
+ end
48
+ end
49
+ end