jekyll-chatgpt-translate 0.0.20 → 0.0.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +0 -1
- data/features/cli.feature +36 -1
- data/jekyll-chatgpt-translate.gemspec +1 -1
- data/lib/jekyll-chatgpt-translate/chatgpt.rb +4 -1
- data/lib/jekyll-chatgpt-translate/generator.rb +25 -15
- data/lib/jekyll-chatgpt-translate/ping.rb +21 -6
- data/lib/jekyll-chatgpt-translate/version.rb +1 -1
- data/test/test__helper.rb +104 -0
- data/test/test_generator.rb +2 -89
- data/test/test_ping.rb +10 -21
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: daa42e099235943fc1a5473a83796abe90b7949d1365c9d9b7349c7c86be7406
|
4
|
+
data.tar.gz: 6c7559aea364339a8b2ac5eac397e314491166b77cfff92565218f6cdd04499b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e651bfae3d35ddf473555eb168cf3301ba6dacda8352fd00a610613c9a2dc8821a1188064089f701ae94c4e4b2277f41b5d635842ae4132f92e9242828ecc0ac
|
7
|
+
data.tar.gz: a18e7007a9f40cfdba340285c8ef665c8c47d8e829ac6f76d304b77cec89b14ce9e9c1d56ddfd72f010429d4620f712ee773ed7011942d771c49993799bcef23
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
[![Gem Version](https://badge.fury.io/rb/glogin.svg)](http://badge.fury.io/rb/glogin)
|
2
1
|
<img src="logo.png" style="width:256px;"/>
|
3
2
|
|
4
3
|
[![rake](https://github.com/yegor256/jekyll-chatgpt-translate/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/jekyll-chatgpt-translate/actions/workflows/rake.yml)
|
data/features/cli.feature
CHANGED
@@ -31,6 +31,11 @@ Feature: Simple site building
|
|
31
31
|
Chinese: {{ content }}
|
32
32
|
The original: {{ page.translated-original-url }}
|
33
33
|
"""
|
34
|
+
And I have a "_layouts/translated.html" file with content:
|
35
|
+
"""
|
36
|
+
French: {{ content }}
|
37
|
+
The original: {{ page.translated-original-url }}
|
38
|
+
"""
|
34
39
|
And I have a "_posts/2023-01-01-hello.md" file with content:
|
35
40
|
"""
|
36
41
|
---
|
@@ -40,6 +45,7 @@ Feature: Simple site building
|
|
40
45
|
Hello, world!
|
41
46
|
"""
|
42
47
|
Then I build Jekyll site
|
48
|
+
And Exit code is zero
|
43
49
|
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" exists
|
44
50
|
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "/2023-01-01-hello-chinese.html"
|
45
51
|
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "translated-language: \"zh\""
|
@@ -48,5 +54,34 @@ Feature: Simple site building
|
|
48
54
|
And File "_site/2023-01-01-hello-chinese.html" exists
|
49
55
|
And File "_site/2023-01-01-hello-chinese.html" contains "The original: /2023/01/01/hello.html"
|
50
56
|
And File "_site/2023/hello-french.html" exists
|
51
|
-
And Exit code is zero
|
52
57
|
|
58
|
+
Scenario: Simple download of existing page
|
59
|
+
Given I have a "_config.yml" file with content:
|
60
|
+
"""
|
61
|
+
url: https://www.yegor256.com
|
62
|
+
markdown: kramdown
|
63
|
+
plugins:
|
64
|
+
- jekyll-chatgpt-translate
|
65
|
+
chatgpt-translate:
|
66
|
+
source: en
|
67
|
+
version: ""
|
68
|
+
api_key: "it-is-not-used, because EN to EN translation"
|
69
|
+
layout: should-not-be-used
|
70
|
+
targets:
|
71
|
+
-
|
72
|
+
language: en
|
73
|
+
permalink: about-me.html
|
74
|
+
"""
|
75
|
+
And I have a "_posts/2023-01-01-hello.md" file with content:
|
76
|
+
"""
|
77
|
+
---
|
78
|
+
title: foo
|
79
|
+
---
|
80
|
+
foo
|
81
|
+
"""
|
82
|
+
Then I build Jekyll site
|
83
|
+
And Exit code is zero
|
84
|
+
And Stdout contains "No need to translate, the page exists"
|
85
|
+
And File "_site/2023/01/01/hello.html" exists
|
86
|
+
And File "_site/about-me.html" exists
|
87
|
+
And File "_site/about-me.html" contains "Yegor Bugayenko"
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
|
28
28
|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
29
29
|
s.required_ruby_version = '>= 2.6'
|
30
30
|
s.name = 'jekyll-chatgpt-translate'
|
31
|
-
s.version = '0.0.
|
31
|
+
s.version = '0.0.22'
|
32
32
|
s.license = 'MIT'
|
33
33
|
s.summary = 'Translate Jekyll Pages Through ChatGPT'
|
34
34
|
s.description = [
|
@@ -50,7 +50,10 @@ class GptTranslate::ChatGPT
|
|
50
50
|
def translate(markdown, min: 32)
|
51
51
|
markdown.split(/\n{2,}/).compact.map do |par|
|
52
52
|
par.strip!
|
53
|
-
if
|
53
|
+
if @source == @target
|
54
|
+
Jekyll.logger.debug("No need to translate from #{@source.inspect} to #{@target.inspect}: #{par.inspect}")
|
55
|
+
par
|
56
|
+
elsif par.length < min
|
54
57
|
Jekyll.logger.debug("Not translating this, b/c too short: #{par.inspect}")
|
55
58
|
par
|
56
59
|
elsif par.start_with?('```') || par.end_with?('```')
|
@@ -58,7 +58,7 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
58
58
|
translated = 0
|
59
59
|
copied = 0
|
60
60
|
model = config['model'] || 'gpt-3.5-turbo'
|
61
|
-
marker = "Translated by ChatGPT #{model}/#{version}"
|
61
|
+
marker = "Translated by ChatGPT #{model}#{version.empty? ? '' : "/#{version}"}"
|
62
62
|
site.posts.docs.shuffle.each do |doc|
|
63
63
|
plain = GptTranslate::Plain.new(doc.content).to_s
|
64
64
|
config['targets'].each do |target|
|
@@ -67,9 +67,22 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
67
67
|
raise 'Language must be defined for each target' if target.nil?
|
68
68
|
path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
|
69
69
|
FileUtils.mkdir_p(File.dirname(path))
|
70
|
-
File.write(
|
71
|
-
|
72
|
-
|
70
|
+
File.write(
|
71
|
+
path,
|
72
|
+
[
|
73
|
+
'---',
|
74
|
+
"layout: #{target['layout'] || layout}",
|
75
|
+
"title: #{doc['title'].to_json}",
|
76
|
+
"description: #{doc['description'].to_json}",
|
77
|
+
"permalink: #{link.to_json}",
|
78
|
+
"translated-original-url: #{doc.url.to_json}",
|
79
|
+
"translated-language: #{lang.to_json}",
|
80
|
+
"chatgpt-model: #{model.to_json}",
|
81
|
+
'---'
|
82
|
+
].join("\n")
|
83
|
+
)
|
84
|
+
ping = GptTranslate::Ping.new(site, link)
|
85
|
+
if config['no_download'].nil? && ping.found?(version.empty? ? '' : marker)
|
73
86
|
copied += 1
|
74
87
|
elsif translated >= threshold
|
75
88
|
next
|
@@ -84,20 +97,12 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
84
97
|
File.write(
|
85
98
|
path,
|
86
99
|
[
|
87
|
-
'---',
|
88
|
-
"layout: #{target['layout'] || layout}",
|
89
|
-
"title: #{doc['title'].to_json}",
|
90
|
-
"description: #{doc['description'].to_json}",
|
91
|
-
"permalink: #{link.to_json}",
|
92
|
-
"translated-original-url: #{doc.url.to_json}",
|
93
|
-
"translated-language: #{lang.to_json}",
|
94
|
-
"chatgpt-model: #{model.to_json}",
|
95
|
-
'---',
|
96
100
|
'',
|
97
101
|
foreign,
|
98
102
|
'',
|
99
103
|
"#{marker} on #{Time.now.strftime('%Y-%m-%d at %H:%M')}\n{: .jekyll-chatgpt-translate}"
|
100
|
-
].join("\n")
|
104
|
+
].join("\n"),
|
105
|
+
mode: 'a+'
|
101
106
|
)
|
102
107
|
site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
|
103
108
|
translated += 1
|
@@ -107,7 +112,8 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
107
112
|
doc.data['chatgpt-model'] = model
|
108
113
|
end
|
109
114
|
end
|
110
|
-
Jekyll.logger.info("
|
115
|
+
Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION}: \
|
116
|
+
#{translated} pages translated and #{copied} pages copied in #{(Time.now - start).round(2)}s")
|
111
117
|
end
|
112
118
|
|
113
119
|
private
|
@@ -128,6 +134,10 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
128
134
|
Jekyll.logger.info("The file with the OpenAI API key is not found: #{file.inspect}")
|
129
135
|
nil
|
130
136
|
end
|
137
|
+
if key.nil? && config['api_key']
|
138
|
+
Jekyll.logger.info("The OpenAI API key is found in 'api_key' of _config.yml")
|
139
|
+
key = config['api_key']
|
140
|
+
end
|
131
141
|
if key.nil? && Jekyll.env == 'development'
|
132
142
|
Jekyll.logger.info("OPENAI_API_KEY environment variable is not set, \
|
133
143
|
the `api_key_file` option is not specified in the _config.yml, and \
|
@@ -25,6 +25,7 @@
|
|
25
25
|
require 'iri'
|
26
26
|
require 'net/http'
|
27
27
|
require 'uri'
|
28
|
+
require 'jekyll'
|
28
29
|
require 'fileutils'
|
29
30
|
require_relative 'version'
|
30
31
|
|
@@ -47,7 +48,7 @@ class GptTranslate::Ping
|
|
47
48
|
@path = path
|
48
49
|
end
|
49
50
|
|
50
|
-
def found?(
|
51
|
+
def found?(marker)
|
51
52
|
home = @site.config['url']
|
52
53
|
return false if home.nil?
|
53
54
|
uri = Iri.new(home).path(@path).to_s
|
@@ -57,20 +58,34 @@ class GptTranslate::Ping
|
|
57
58
|
html = before.body
|
58
59
|
if html.include?(marker)
|
59
60
|
Jekyll.logger.info("No need to translate, the page exists at \
|
60
|
-
#{uri.inspect} (#{html.split.count} words)
|
61
|
-
|
62
|
-
File.write(file, html)
|
61
|
+
#{uri.inspect} (#{html.split.count} words)")
|
62
|
+
@site.static_files << DownloadedFile.new(@site, @path, html)
|
63
63
|
return true
|
64
64
|
end
|
65
65
|
Jekyll.logger.info("Re-translation required for #{uri.inspect}")
|
66
66
|
else
|
67
|
-
Jekyll.logger.info("The page is absent, will translate #{uri.inspect}")
|
67
|
+
Jekyll.logger.info("The page is absent, will translate #{uri.inspect} (#{before.code})")
|
68
68
|
end
|
69
69
|
Jekyll.logger.debug("GET #{uri.inspect}: #{before.code}")
|
70
|
-
rescue
|
70
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL => e
|
71
71
|
Jekyll.logger.debug("Failed to ping #{uri.inspect}: #{e.message}")
|
72
72
|
Jekyll.logger.info("The page is absent (#{e.class.name}): #{uri.inspect}")
|
73
73
|
end
|
74
74
|
false
|
75
75
|
end
|
76
|
+
|
77
|
+
# The file we just downloaded.
|
78
|
+
class DownloadedFile < Jekyll::StaticFile
|
79
|
+
def initialize(site, path, html)
|
80
|
+
super(site, site.dest, '', path)
|
81
|
+
@html = html
|
82
|
+
end
|
83
|
+
|
84
|
+
def write(_dest)
|
85
|
+
FileUtils.mkdir_p(File.dirname(path))
|
86
|
+
File.write(path, @html)
|
87
|
+
Jekyll.logger.info("Saved #{@html.split.count} words to #{path.inspect}")
|
88
|
+
true
|
89
|
+
end
|
90
|
+
end
|
76
91
|
end
|
data/test/test__helper.rb
CHANGED
@@ -27,3 +27,107 @@ SimpleCov.start
|
|
27
27
|
|
28
28
|
require 'jekyll'
|
29
29
|
Jekyll.logger.adjust_verbosity(verbose: true)
|
30
|
+
|
31
|
+
# The module we are in.
|
32
|
+
module GptTranslate; end
|
33
|
+
|
34
|
+
# Fake.
|
35
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
36
|
+
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
37
|
+
# License:: MIT
|
38
|
+
class GptTranslate::FakeSite
|
39
|
+
attr_reader :config, :pages, :static_files
|
40
|
+
|
41
|
+
def initialize(config, docs = [])
|
42
|
+
@config = config
|
43
|
+
@docs = docs
|
44
|
+
@pages = []
|
45
|
+
@static_files = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def posts
|
49
|
+
GptTranslate::FakePosts.new(@docs)
|
50
|
+
end
|
51
|
+
|
52
|
+
def permalink_style
|
53
|
+
''
|
54
|
+
end
|
55
|
+
|
56
|
+
def frontmatter_defaults
|
57
|
+
Jekyll::FrontmatterDefaults.new(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def converters
|
61
|
+
[Jekyll::Converters::Markdown.new({ 'markdown_ext' => 'md' })]
|
62
|
+
end
|
63
|
+
|
64
|
+
def source
|
65
|
+
''
|
66
|
+
end
|
67
|
+
|
68
|
+
def dest
|
69
|
+
return '' if @docs.empty?
|
70
|
+
File.dirname(@docs[0])
|
71
|
+
end
|
72
|
+
|
73
|
+
def in_theme_dir(base, _foo = nil, _bar = nil)
|
74
|
+
base
|
75
|
+
end
|
76
|
+
|
77
|
+
def in_dest_dir(*paths)
|
78
|
+
paths[0].dup
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Fake.
|
83
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
84
|
+
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
85
|
+
# License:: MIT
|
86
|
+
class GptTranslate::FakeDocument
|
87
|
+
attr_reader :data
|
88
|
+
|
89
|
+
def initialize(path)
|
90
|
+
@path = path
|
91
|
+
@data = { 'date' => Time.now, 'title' => 'Hello!' }
|
92
|
+
end
|
93
|
+
|
94
|
+
def content
|
95
|
+
'Hello, world!'
|
96
|
+
end
|
97
|
+
|
98
|
+
def []=(key, value)
|
99
|
+
@data[key] = value
|
100
|
+
end
|
101
|
+
|
102
|
+
def [](key)
|
103
|
+
@data[key] || ''
|
104
|
+
end
|
105
|
+
|
106
|
+
def relative_path
|
107
|
+
@path
|
108
|
+
end
|
109
|
+
|
110
|
+
def url
|
111
|
+
'2023-01-01-hello.html'
|
112
|
+
end
|
113
|
+
|
114
|
+
def basename
|
115
|
+
'2023-01-01-hello.md'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Fake.
|
120
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
121
|
+
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
122
|
+
# License:: MIT
|
123
|
+
class GptTranslate::FakePosts
|
124
|
+
attr_reader :config
|
125
|
+
|
126
|
+
def initialize(docs)
|
127
|
+
@docs = docs
|
128
|
+
end
|
129
|
+
|
130
|
+
def docs
|
131
|
+
@docs.map { |d| GptTranslate::FakeDocument.new(d) }
|
132
|
+
end
|
133
|
+
end
|
data/test/test_generator.rb
CHANGED
@@ -33,98 +33,11 @@ require_relative '../lib/jekyll-chatgpt-translate/generator'
|
|
33
33
|
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
34
34
|
# License:: MIT
|
35
35
|
class GptTranslate::GeneratorTest < Minitest::Test
|
36
|
-
class FakeSite
|
37
|
-
attr_reader :config, :pages
|
38
|
-
|
39
|
-
def initialize(config, docs)
|
40
|
-
@config = config
|
41
|
-
@docs = docs
|
42
|
-
@pages = []
|
43
|
-
end
|
44
|
-
|
45
|
-
def posts
|
46
|
-
FakePosts.new(@docs)
|
47
|
-
end
|
48
|
-
|
49
|
-
def permalink_style
|
50
|
-
''
|
51
|
-
end
|
52
|
-
|
53
|
-
def frontmatter_defaults
|
54
|
-
Jekyll::FrontmatterDefaults.new(self)
|
55
|
-
end
|
56
|
-
|
57
|
-
def converters
|
58
|
-
[Jekyll::Converters::Markdown.new({ 'markdown_ext' => 'md' })]
|
59
|
-
end
|
60
|
-
|
61
|
-
def source
|
62
|
-
''
|
63
|
-
end
|
64
|
-
|
65
|
-
def dest
|
66
|
-
File.dirname(@docs[0])
|
67
|
-
end
|
68
|
-
|
69
|
-
def in_theme_dir(base, _foo = nil, _bar = nil)
|
70
|
-
base
|
71
|
-
end
|
72
|
-
|
73
|
-
def in_dest_dir(*paths)
|
74
|
-
paths[0].dup
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
class FakeDocument
|
79
|
-
attr_reader :data
|
80
|
-
|
81
|
-
def initialize(path)
|
82
|
-
@path = path
|
83
|
-
@data = { 'date' => Time.now, 'title' => 'Hello!' }
|
84
|
-
end
|
85
|
-
|
86
|
-
def content
|
87
|
-
'Hello, world!'
|
88
|
-
end
|
89
|
-
|
90
|
-
def []=(key, value)
|
91
|
-
@data[key] = value
|
92
|
-
end
|
93
|
-
|
94
|
-
def [](key)
|
95
|
-
@data[key] || ''
|
96
|
-
end
|
97
|
-
|
98
|
-
def relative_path
|
99
|
-
@path
|
100
|
-
end
|
101
|
-
|
102
|
-
def url
|
103
|
-
'2023-01-01-hello.html'
|
104
|
-
end
|
105
|
-
|
106
|
-
def basename
|
107
|
-
'2023-01-01-hello.md'
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
class FakePosts
|
112
|
-
attr_reader :config
|
113
|
-
|
114
|
-
def initialize(docs)
|
115
|
-
@docs = docs
|
116
|
-
end
|
117
|
-
|
118
|
-
def docs
|
119
|
-
@docs.map { |d| FakeDocument.new(d) }
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
36
|
def test_simple_scenario
|
124
37
|
Dir.mktmpdir do |home|
|
125
38
|
post = File.join(home, '2023-01-01-hello.md')
|
126
39
|
File.write(post, "---\ntitle: Hello\n---\n\nHello, world!")
|
127
|
-
site = FakeSite.new(
|
40
|
+
site = GptTranslate::FakeSite.new(
|
128
41
|
{
|
129
42
|
'url' => 'https://www.yegor256.com/',
|
130
43
|
'chatgpt-translate' => {
|
@@ -150,7 +63,7 @@ class GptTranslate::GeneratorTest < Minitest::Test
|
|
150
63
|
Dir.mktmpdir do |home|
|
151
64
|
post = File.join(home, '2023-01-01-hello.md')
|
152
65
|
File.write(post, "---\ntitle: Hello\n---\n\nHello, world!")
|
153
|
-
site = FakeSite.new(
|
66
|
+
site = GptTranslate::FakeSite.new(
|
154
67
|
{
|
155
68
|
'chatgpt-translate' => {
|
156
69
|
'threshold' => 1,
|
data/test/test_ping.rb
CHANGED
@@ -34,39 +34,28 @@ require_relative '../lib/jekyll-chatgpt-translate/ping'
|
|
34
34
|
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
35
35
|
# License:: MIT
|
36
36
|
class GptTranslate::PingTest < Minitest::Test
|
37
|
-
class FakeSite
|
38
|
-
attr_reader :config
|
39
|
-
|
40
|
-
def initialize(config)
|
41
|
-
@config = config
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
37
|
def test_when_exists
|
46
|
-
stub_request(:any, 'https://www.yegor256.com/about-me.html')
|
47
|
-
site = FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
|
38
|
+
stub_request(:any, 'https://www.yegor256.com/about-me.html').to_return(body: 'Hello!')
|
39
|
+
site = GptTranslate::FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
|
48
40
|
ping = GptTranslate::Ping.new(site, '/about-me.html')
|
49
|
-
|
50
|
-
|
51
|
-
end
|
41
|
+
assert(ping.found?(''))
|
42
|
+
assert_equal(1, site.static_files.size)
|
52
43
|
end
|
53
44
|
|
54
45
|
def test_when_not_exists
|
55
46
|
stub_request(:any, 'https://www.yegor256.com/absent.html').to_return(status: 404)
|
56
|
-
site = FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
|
47
|
+
site = GptTranslate::FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
|
57
48
|
ping = GptTranslate::Ping.new(site, '/absent.html')
|
58
|
-
|
59
|
-
|
60
|
-
end
|
49
|
+
assert(!ping.found?(''))
|
50
|
+
assert_equal(0, site.static_files.size)
|
61
51
|
end
|
62
52
|
|
63
53
|
def test_wrong_address
|
64
54
|
WebMock.allow_net_connect!
|
65
|
-
site = FakeSite.new({ 'url' => 'https://localhost:1/' })
|
55
|
+
site = GptTranslate::FakeSite.new({ 'url' => 'https://localhost:1/' })
|
66
56
|
ping = GptTranslate::Ping.new(site, '/boom.html')
|
67
|
-
|
68
|
-
|
69
|
-
end
|
57
|
+
assert(!ping.found?(''))
|
58
|
+
assert_equal(0, site.static_files.size)
|
70
59
|
end
|
71
60
|
|
72
61
|
def test_relative_path
|