trmnl_preview 0.30.2 → 0.30.3
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/CHANGELOG.md +5 -0
- data/lib/trmnlp/testing/asset_cache.rb +127 -0
- data/lib/trmnlp/testing/browser.rb +6 -3
- data/lib/trmnlp/testing/rspec.rb +8 -1
- data/lib/trmnlp/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be8279b91f8e33f4750e7176131b29d393fe8a920f4313bcd6770af6fca278dc
|
|
4
|
+
data.tar.gz: aef27ba9bf23146aa0a52a2b8374e5c521f90b20a164247696a290c8d99b161e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76dd26fb7ad267c2a62ddc34e0ad5b220a3adea743dcfe2aac96b3de4d915c8d1a29cbf4b39bb0900c395ddb781015269f7fa913b278f79aec56010c790a82e5
|
|
7
|
+
data.tar.gz: 26f62dff933de915714295a8ae80530a2a567dabf4e4f676559dc5c20fc18c9fb780a3659fa9d5dadf5e63afaf6b4117989a16177727af19820a8290c26beca9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
# Changelog
|
|
3
3
|
|
|
4
|
+
## 0.30.3
|
|
5
|
+
|
|
6
|
+
- `trmnlp test` serves the Framework's stylesheets, scripts and fonts from a cache in `~/.cache/trmnl/assets`, downloaded from trmnl.com once and kept as long as trmnl.com says. A run no longer fails when trmnl.com is slow or down, as long as the files were fetched before; when a file is due for a refresh and trmnl.com cannot be reached, the copy on disk is used.
|
|
7
|
+
- `trmnlp test` also serves the Google Fonts stylesheet the Framework links for Inter, and its font files, from the same cache, asked for as Firefox asks so Google sends the same fonts. A test page now loads nothing from outside unless the recipe itself does.
|
|
8
|
+
|
|
4
9
|
## 0.30.2
|
|
5
10
|
|
|
6
11
|
- `trmnlp serve`'s HTML preview of a device TRMNL scales runs the page's scripts at the CSS size too, then shows the page at the device's size and stops its timers, as the PNG and the device do. Before, a table showed 8 of 20 rows on a TRMNL X where the device shows 15.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
require 'socket'
|
|
8
|
+
|
|
9
|
+
require_relative '../framework_version'
|
|
10
|
+
|
|
11
|
+
module TRMNLP
|
|
12
|
+
module Testing
|
|
13
|
+
# Serves the Framework's files and Google Fonts to test pages from disk, for as long as their hosts say.
|
|
14
|
+
class AssetCache
|
|
15
|
+
HOST = FrameworkVersion::DEFAULT_ASSET_HOST
|
|
16
|
+
FRAMEWORK_LINK = %r{#{Regexp.escape(HOST)}(?=/(?:css|js)/)}
|
|
17
|
+
GOOGLE_FONTS_LINK = %r{https://(fonts\.(?:googleapis|gstatic)\.com)/}
|
|
18
|
+
SAFE_PATH = %r{\A/(?:[\w.-]+/)*[\w.-]+\z}
|
|
19
|
+
DOWNLOAD_TIMEOUT_SECONDS = 10
|
|
20
|
+
BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n"
|
|
21
|
+
|
|
22
|
+
def initialize(dir:, clock: -> { Time.now })
|
|
23
|
+
@dir = dir.to_s
|
|
24
|
+
@clock = clock
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
@server = TCPServer.new('127.0.0.1', 0)
|
|
29
|
+
@thread = Thread.new { loop { Thread.new(@server.accept) { serve(it) } } }
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop
|
|
34
|
+
@thread&.kill
|
|
35
|
+
@server&.close
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def url = "http://127.0.0.1:#{@server.addr[1]}"
|
|
39
|
+
|
|
40
|
+
def point_framework_links_at_cache(html) = point_google_fonts_at_cache(html.gsub(FRAMEWORK_LINK, url))
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def point_google_fonts_at_cache(text) = text.gsub(GOOGLE_FONTS_LINK) { "#{url}/#{::Regexp.last_match(1)}/" }
|
|
45
|
+
|
|
46
|
+
def serve(socket)
|
|
47
|
+
path, query = socket.gets.to_s.split[1].to_s.split('?', 2)
|
|
48
|
+
user_agent = nil
|
|
49
|
+
while (line = socket.gets) && !line.strip.empty?
|
|
50
|
+
user_agent = line.split(':', 2).last.strip if line.downcase.start_with?('user-agent:')
|
|
51
|
+
end
|
|
52
|
+
asset = !path.to_s.include?('..') && path.to_s.match?(SAFE_PATH) && asset(path, query, user_agent)
|
|
53
|
+
socket.write(asset ? response(asset) : BAD_GATEWAY)
|
|
54
|
+
rescue IOError, SystemCallError
|
|
55
|
+
nil
|
|
56
|
+
ensure
|
|
57
|
+
socket.close unless socket.closed?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def asset(path, query, user_agent)
|
|
61
|
+
key = query ? "#{path}.#{Digest::SHA256.hexdigest(query)[0, 16]}" : path
|
|
62
|
+
cached = cached(key)
|
|
63
|
+
return cached if cached && cached['expires_at'] > @clock.call.to_i
|
|
64
|
+
|
|
65
|
+
download(key, URI("#{upstream(path)}#{"?#{query}" if query}"), user_agent) || cached
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A Google Fonts path starts with its host, /fonts.gstatic.com/s/...; any other path is trmnl.com's.
|
|
69
|
+
def upstream(path)
|
|
70
|
+
host, rest = path.delete_prefix('/').split('/', 2)
|
|
71
|
+
host.match?(/\Afonts\.(googleapis|gstatic)\.com\z/) ? "https://#{host}/#{rest}" : "#{HOST}#{path}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cached(key)
|
|
75
|
+
meta = JSON.parse(File.read(file(key, '.json')))
|
|
76
|
+
meta.merge('body' => File.binread(file(key)))
|
|
77
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# trmnl.com sends its CORS header only to a request with an Origin; Google picks the font format by User-Agent.
|
|
82
|
+
def download(key, uri, user_agent)
|
|
83
|
+
headers = { 'Origin' => url, 'User-Agent' => user_agent }.compact
|
|
84
|
+
timeouts = { open_timeout: DOWNLOAD_TIMEOUT_SECONDS, read_timeout: DOWNLOAD_TIMEOUT_SECONDS }
|
|
85
|
+
answer = Net::HTTP.start(uri.host, uri.port, use_ssl: true, **timeouts) { it.get(uri.request_uri, headers) }
|
|
86
|
+
answer.is_a?(Net::HTTPSuccess) ? cache_download(key, answer) : nil
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cache_download(path, answer)
|
|
92
|
+
meta = { 'content_type' => answer['content-type'], 'cache_control' => answer['cache-control'],
|
|
93
|
+
'allow_origin' => answer['access-control-allow-origin'],
|
|
94
|
+
'expires_at' => @clock.call.to_i + answer['cache-control'].to_s[/max-age=(\d+)/, 1].to_i }
|
|
95
|
+
write_to_disk(path, answer.body, meta)
|
|
96
|
+
meta.merge('body' => answer.body)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def write_to_disk(path, body, meta)
|
|
100
|
+
write(file(path), body)
|
|
101
|
+
write(file(path, '.json'), JSON.generate(meta))
|
|
102
|
+
rescue SystemCallError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Written beside the file and renamed over it, so another run reading the cache sees a whole file.
|
|
107
|
+
def write(path, content)
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
109
|
+
temporary = "#{path}.#{Process.pid}.#{Thread.current.object_id}"
|
|
110
|
+
File.binwrite(temporary, content)
|
|
111
|
+
File.rename(temporary, path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def file(path, extension = '') = File.join(@dir, "#{path}#{extension}")
|
|
115
|
+
|
|
116
|
+
# A stylesheet's Google font files are pointed at the cache as it is served, since the cache's port changes.
|
|
117
|
+
def response(asset)
|
|
118
|
+
stylesheet = asset['content_type'].to_s.start_with?('text/css')
|
|
119
|
+
body = stylesheet ? point_google_fonts_at_cache(asset['body']) : asset['body']
|
|
120
|
+
headers = { 'content-type' => asset['content_type'], 'cache-control' => asset['cache_control'],
|
|
121
|
+
'access-control-allow-origin' => asset['allow_origin'],
|
|
122
|
+
'content-length' => body.bytesize, 'connection' => 'close' }.compact
|
|
123
|
+
"HTTP/1.1 200 OK\r\n#{headers.map { "#{it[0]}: #{it[1]}\r\n" }.join}\r\n#{body}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -13,9 +13,10 @@ module TRMNLP
|
|
|
13
13
|
# The Firefox one example renders in, checked out of the pool on first use and back in by #release.
|
|
14
14
|
# Pages are drawn through ScreenGenerator and Screenshot like `trmnlp build --png`'s, but opened from page_server.
|
|
15
15
|
class Browser
|
|
16
|
-
def initialize(pool:, page_server:)
|
|
16
|
+
def initialize(pool:, page_server:, asset_cache:)
|
|
17
17
|
@pool = pool
|
|
18
18
|
@page_server = page_server
|
|
19
|
+
@asset_cache = asset_cache
|
|
19
20
|
@screenshot = Screenshot.new(pool:)
|
|
20
21
|
@pages = {}.compare_by_identity
|
|
21
22
|
@fresh_browsers = []
|
|
@@ -24,13 +25,15 @@ module TRMNLP
|
|
|
24
25
|
# A Firefox of its own, with nothing cached, closed when this browser is released.
|
|
25
26
|
def fresh
|
|
26
27
|
pool = BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
|
|
27
|
-
self.class.new(pool:, page_server: @page_server
|
|
28
|
+
browser = self.class.new(pool:, page_server: @page_server, asset_cache: @asset_cache)
|
|
29
|
+
browser.tap { @fresh_browsers << [it, pool] }
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# Loads the screen's page; its picture is only taken when a test asks for it.
|
|
31
33
|
def load(screen)
|
|
32
34
|
generator = generator(screen)
|
|
33
|
-
|
|
35
|
+
html = @asset_cache.point_framework_links_at_cache(generator.html)
|
|
36
|
+
@pages[screen] = [html, generator.width, generator.height, @page_server.add(html)]
|
|
34
37
|
show(screen)
|
|
35
38
|
end
|
|
36
39
|
|
data/lib/trmnlp/testing/rspec.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'capybara/rspec/matchers' # after rspec/expectations, or its matchers lo
|
|
|
8
8
|
require_relative '../../trmnlp'
|
|
9
9
|
require_relative '../browser_pool'
|
|
10
10
|
require_relative '../firefox_driver'
|
|
11
|
+
require_relative 'asset_cache'
|
|
11
12
|
require_relative 'browser'
|
|
12
13
|
require_relative 'certificate_authority'
|
|
13
14
|
require_relative 'ink_count'
|
|
@@ -33,6 +34,11 @@ module TRMNLP
|
|
|
33
34
|
@browser_pool ||= BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
def self.asset_cache
|
|
38
|
+
@asset_cache ||= AssetCache.new(dir: Paths.new(plugin_dir).cache_dir.join('assets')).start
|
|
39
|
+
.tap { |cache| at_exit { cache.stop } }
|
|
40
|
+
end
|
|
41
|
+
|
|
36
42
|
def self.page_server
|
|
37
43
|
@page_server ||= PageServer.new.start.tap { |server| at_exit { server.stop } }
|
|
38
44
|
end
|
|
@@ -41,7 +47,8 @@ module TRMNLP
|
|
|
41
47
|
def trmnl = @trmnl ||= Plugin.new(Testing.plugin_dir, browser: trmnl_browser, authority: Testing.authority)
|
|
42
48
|
|
|
43
49
|
def trmnl_browser
|
|
44
|
-
@trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server
|
|
50
|
+
@trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server,
|
|
51
|
+
asset_cache: Testing.asset_cache)
|
|
45
52
|
end
|
|
46
53
|
end
|
|
47
54
|
end
|
data/lib/trmnlp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trmnl_preview
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.30.
|
|
4
|
+
version: 0.30.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rockwell Schrock
|
|
@@ -476,6 +476,7 @@ files:
|
|
|
476
476
|
- lib/trmnlp/screen_generator.rb
|
|
477
477
|
- lib/trmnlp/screenshot.rb
|
|
478
478
|
- lib/trmnlp/select_field_choices.rb
|
|
479
|
+
- lib/trmnlp/testing/asset_cache.rb
|
|
479
480
|
- lib/trmnlp/testing/browser.rb
|
|
480
481
|
- lib/trmnlp/testing/certificate_authority.rb
|
|
481
482
|
- lib/trmnlp/testing/device_models.rb
|