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.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +53 -0
- data/.devcontainer/devcontainer.json +51 -0
- data/.devcontainer/docker-compose.yml +33 -0
- data/.ruby-gemset +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +93 -6
- data/README.md +80 -5
- data/docker/Dockerfile +1 -1
- data/docker/Dockerfile.slim +2 -2
- data/docker/chrome-driver/chrome.json +874 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +97 -4
- data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +178 -0
- data/lib/bidi2pdf/bidi/client.rb +12 -5
- data/lib/bidi2pdf/bidi/commands/base.rb +2 -0
- data/lib/bidi2pdf/bidi/commands/capture_screenshot.rb +33 -0
- data/lib/bidi2pdf/bidi/commands/set_viewport.rb +32 -0
- data/lib/bidi2pdf/bidi/commands.rb +2 -0
- data/lib/bidi2pdf/bidi/network_event.rb +12 -3
- data/lib/bidi2pdf/bidi/network_events.rb +9 -1
- data/lib/bidi2pdf/bidi/session.rb +1 -1
- data/lib/bidi2pdf/chromedriver_manager.rb +10 -6
- data/lib/bidi2pdf/cli.rb +11 -1
- data/lib/bidi2pdf/launcher.rb +5 -3
- data/lib/bidi2pdf/notifications/logging_subscriber.rb +2 -0
- data/lib/bidi2pdf/session_warmer.rb +377 -0
- data/lib/bidi2pdf/test_helpers/images.rb +8 -0
- data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +1 -2
- data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +1 -2
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +1 -1
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +22 -2
- data/lib/bidi2pdf/test_helpers/testcontainers/container_endpoint.rb +30 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb +33 -0
- data/lib/bidi2pdf/test_helpers/testcontainers.rb +1 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +26 -1
- data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +16 -9
- data/sig/bidi2pdf/bidi/auth_interceptor.rbs +32 -7
- data/sig/bidi2pdf/bidi/browser.rbs +4 -31
- data/sig/bidi2pdf/bidi/browser_console_logger.rbs +59 -0
- data/sig/bidi2pdf/bidi/browser_tab.rbs +269 -25
- data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +81 -0
- data/sig/bidi2pdf/bidi/client.rbs +103 -43
- data/sig/bidi2pdf/bidi/command_manager.rbs +5 -17
- data/sig/bidi2pdf/bidi/commands/add_intercept.rbs +16 -8
- data/sig/bidi2pdf/bidi/commands/base.rbs +49 -9
- data/sig/bidi2pdf/bidi/commands/browser_close.rbs +3 -1
- data/sig/bidi2pdf/bidi/commands/browser_create_user_context.rbs +2 -0
- data/sig/bidi2pdf/bidi/commands/browser_remove_user_context.rbs +19 -0
- data/sig/bidi2pdf/bidi/commands/browsing_context_close.rbs +8 -2
- data/sig/bidi2pdf/bidi/commands/browsing_context_navigate.rbs +12 -6
- data/sig/bidi2pdf/bidi/commands/browsing_context_print.rbs +10 -5
- data/sig/bidi2pdf/bidi/commands/cancel_auth.rbs +8 -2
- data/sig/bidi2pdf/bidi/commands/capture_screenshot.rbs +31 -0
- data/sig/bidi2pdf/bidi/commands/cdp_get_session.rbs +17 -0
- data/sig/bidi2pdf/bidi/commands/create_window.rbs +4 -0
- data/sig/bidi2pdf/bidi/commands/get_user_contexts.rbs +2 -0
- data/sig/bidi2pdf/bidi/commands/network_continue.rbs +4 -0
- data/sig/bidi2pdf/bidi/commands/page_print.rbs +28 -0
- data/sig/bidi2pdf/bidi/commands/print_parameters_validator.rbs +3 -0
- data/sig/bidi2pdf/bidi/commands/provide_credentials.rbs +12 -6
- data/sig/bidi2pdf/bidi/commands/script_evaluate.rbs +4 -0
- data/sig/bidi2pdf/bidi/commands/session_end.rbs +2 -0
- data/sig/bidi2pdf/bidi/commands/session_status.rbs +3 -1
- data/sig/bidi2pdf/bidi/commands/session_subscribe.rbs +4 -0
- data/sig/bidi2pdf/bidi/commands/set_tab_cookie.rbs +51 -23
- data/sig/bidi2pdf/bidi/commands/set_usercontext_cookie.rbs +17 -19
- data/sig/bidi2pdf/bidi/commands/set_viewport.rbs +31 -0
- data/sig/bidi2pdf/bidi/connection_manager.rbs +2 -1
- data/sig/bidi2pdf/bidi/event_manager.rbs +14 -19
- data/sig/bidi2pdf/bidi/interceptor.rbs +5 -0
- data/sig/bidi2pdf/bidi/js_logger_helper.rbs +9 -0
- data/sig/bidi2pdf/bidi/logger_events.rbs +21 -0
- data/sig/bidi2pdf/bidi/navigation_failed_events.rbs +17 -0
- data/sig/bidi2pdf/bidi/network_event.rbs +64 -46
- data/sig/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rbs +63 -0
- data/sig/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rbs +15 -0
- data/sig/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rbs +18 -0
- data/sig/bidi2pdf/bidi/network_event_formatters.rbs +6 -0
- data/sig/bidi2pdf/bidi/network_events.rbs +14 -39
- data/sig/bidi2pdf/bidi/session.rbs +144 -31
- data/sig/bidi2pdf/bidi/user_context.rbs +64 -45
- data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +22 -30
- data/sig/bidi2pdf/chromedriver_manager.rbs +61 -22
- data/sig/bidi2pdf/cli.rbs +22 -2
- data/sig/bidi2pdf/dsl.rbs +34 -0
- data/sig/bidi2pdf/launcher.rbs +39 -1
- data/sig/bidi2pdf/notifications/event.rbs +40 -0
- data/sig/bidi2pdf/notifications/instrumenter.rbs +21 -0
- data/sig/bidi2pdf/notifications/logging_subscriber.rbs +41 -0
- data/sig/bidi2pdf/notifications.rbs +24 -0
- data/sig/bidi2pdf/process_tree.rbs +2 -2
- data/sig/bidi2pdf/session_runner.rbs +41 -1
- data/sig/bidi2pdf/session_warmer.rbs +221 -0
- data/sig/bidi2pdf/test_helpers/configuration.rbs +51 -0
- data/sig/bidi2pdf/test_helpers/images/extractor.rbs +43 -0
- data/sig/bidi2pdf/test_helpers/images/image_similarity_checker.rbs +35 -0
- data/sig/bidi2pdf/test_helpers/images/tiff_helper.rbs +70 -0
- data/sig/bidi2pdf/test_helpers/images.rbs +0 -0
- data/sig/bidi2pdf/test_helpers/matchers/contains_pdf_image.rbs +0 -0
- data/sig/bidi2pdf/test_helpers/matchers/contains_pdf_text.rbs +0 -0
- data/sig/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rbs +0 -0
- data/sig/bidi2pdf/test_helpers/matchers/match_pdf_text.rbs +0 -0
- data/sig/bidi2pdf/test_helpers/pdf_file_helper.rbs +23 -0
- data/sig/bidi2pdf/test_helpers/pdf_reader_utils.rbs +49 -0
- data/sig/bidi2pdf/test_helpers/pdf_text_sanitizer.rbs +38 -0
- data/sig/bidi2pdf/test_helpers/spec_paths_helper.rbs +35 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rbs +43 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rbs +45 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/container_endpoint.rbs +25 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rbs +3 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rbs +47 -0
- data/sig/bidi2pdf/test_helpers/testcontainers.rbs +6 -0
- data/sig/bidi2pdf/test_helpers.rbs +0 -0
- data/sig/bidi2pdf/verbose_logger.rbs +37 -0
- data/sig/bidi2pdf/version.rbs +3 -0
- data/sig/bidi2pdf.rbs +130 -2
- data/tasks/generate_rbs.rake +11 -11
- data/tasks/release_credentials_check.rake +97 -0
- metadata +67 -10
- 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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|