Skip to content

Commit ff221c7

Browse files
committed
chore: add specs for forge
1 parent 4c3b9d6 commit ff221c7

File tree

5 files changed

+107
-43
lines changed

5 files changed

+107
-43
lines changed

lib/forge.rb

+7-5
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,21 @@ def forge(concept)
2424
# Find things that need to be deleted and things that are up to date
2525
synthesizers.each do |synthesizer|
2626
template = synthesizer.path_template(concept)
27-
existing_file = template.existing_file
27+
existing_signature = template.existing_signature
2828

2929
# File does not exist, can not be up to date
30-
next unless existing_file
30+
next unless existing_signature
3131

3232
# File exists but the synthesizer no longer works on it, delete it
3333
unless synthesizer.synthesizes?(concept)
34-
next pending_deletions << existing_file
34+
next pending_deletions << template.full_path(existing_signature)
3535
end
3636

37-
file_signature = template.parse_signature_from_file(existing_file)
3837
# File exists and the synthesizer has pending work
39-
next up_to_date << synthesizer if file_signature != concept.signature
38+
next up_to_date << synthesizer if existing_signature == concept.signature
39+
40+
# File exists but the signature is different, delete it
41+
pending_deletions << template.full_path(existing_signature)
4042
end
4143

4244
# Delete generated files that are no longer needed

lib/forge/path_template.rb

+10-16
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def initialize(path:, basename:, extension:)
2020
@basename = basename
2121
@extension = extension
2222
@filename_regex = T.let(nil, T.nilable(Regexp))
23-
@existing_file = T.let(nil, T.nilable(String))
23+
@existing_signature = T.let(nil, T.nilable(String))
2424
end
2525

2626
sig { params(signature: String).returns(String) }
@@ -33,30 +33,24 @@ def full_path(signature)
3333
File.join(path, filename(signature))
3434
end
3535

36-
sig { params(path: String).returns(String) }
37-
def parse_signature_from_file(path)
38-
match = path.match(filename_regex)
39-
unless match
40-
raise BadSignatureError, "Cannot parse signature from #{path}"
41-
end
42-
T.must(match[1])
43-
end
44-
4536
sig { returns(T.nilable(String)) }
46-
def existing_file
47-
@existing_file ||= find_existing_file
37+
def existing_signature
38+
@existing_signature ||= find_existing_signature
4839
end
4940

5041
private
5142

5243
sig { returns(T.nilable(String)) }
53-
def find_existing_file
44+
def find_existing_signature
5445
signatures =
5546
Dir
5647
.glob(glob)
5748
.filter_map do |path|
58-
match = path.match(filename_regex)
59-
raise BadSignatureError, "Bad signature found: #{path}" unless match
49+
basename = File.basename(path)
50+
match = basename.match(filename_regex)
51+
unless match
52+
raise BadSignatureError, "Bad signature found: #{basename}"
53+
end
6054
match[1]
6155
end
6256

@@ -73,7 +67,7 @@ def glob
7367

7468
sig { returns(Regexp) }
7569
def filename_regex
76-
@filename_regex ||= Regexp.new("#{basename}\.(\H+)\.#{extension}", "i")
70+
@filename_regex ||= Regexp.new("#{basename}\\.(\\h+)\\.#{extension}", "i")
7771
end
7872

7973
sig { returns(String) }

spec/forge_spec.rb

+65-15
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,80 @@
22
# typed: false
33

44
require "spec_helper"
5-
require_relative "./fake_synthesizer"
5+
require_relative "./mock_synthesizer"
6+
require_relative "./mock_concept"
67
require_relative "../lib/forge"
8+
require "fileutils"
79

810
RSpec.describe Forge do
911
let(:forge) { Forge.new }
10-
let(:concept) { FakeConcept.new }
12+
let(:concept) { MockConcept.new }
13+
let(:path_template) do
14+
Forge::PathTemplate.new(
15+
path: MockSynthesizer::PATH,
16+
basename: MockSynthesizer::BASENAME,
17+
extension: MockSynthesizer::EXTENSION
18+
)
19+
end
20+
let(:synthesizer) { MockSynthesizer.new }
1121

1222
describe "#forge" do
13-
before { forge.register_synthesizer(FakeSynthesizer.new) }
23+
before do
24+
if Dir.exist?(MockSynthesizer::PATH)
25+
FileUtils.rm_rf(MockSynthesizer::PATH)
26+
end
27+
28+
forge.register_synthesizer(synthesizer)
29+
FileUtils.mkdir_p(MockSynthesizer::PATH)
30+
end
1431

15-
subject! { forge.forge(concept) }
32+
after { FileUtils.rm_rf(MockSynthesizer::PATH) }
1633

1734
it "forges an artifact" do
18-
expect(
19-
File.exist?(
20-
File.join(
21-
Forge::PathTemplate.new(
22-
path: FakeSynthesizer.PATH,
23-
basename: FakeSynthesizer.BASENAME,
24-
extension: FakeSynthesizer.EXTENSION
25-
).full_path(concept.signature)
26-
)
27-
)
28-
).to be_truthy
35+
forge.forge(concept)
36+
expected_path = path_template.full_path(concept.signature)
37+
expect(File.exist?(expected_path)).to be_truthy
38+
expect(File.read(expected_path)).to eq(synthesizer.content)
39+
end
40+
41+
it "deletes an artifact" do
42+
forge.forge(concept)
43+
expected_path = path_template.full_path(concept.signature)
44+
expect(File.exist?(expected_path)).to be_truthy
45+
46+
synthesizer.synthesizes = false
47+
forge.forge(concept)
48+
expect(File.exist?(expected_path)).to be_falsey
49+
end
50+
51+
it "rebuilds an artifact when signature changes" do
52+
forge.forge(concept)
53+
initial_path = path_template.full_path(concept.signature)
54+
55+
expect(File.exist?(initial_path)).to be_truthy
56+
expect(File.read(initial_path)).to eq(synthesizer.content)
57+
58+
concept.dump = { foo: "bar" }
59+
synthesizer.content = "A different content"
60+
expected_path = path_template.full_path(concept.signature)
61+
forge.forge(concept)
62+
63+
expect(File.exist?(initial_path)).to be_falsey
64+
expect(File.exist?(expected_path)).to be_truthy
65+
expect(File.read(expected_path)).to eq(synthesizer.content)
66+
end
67+
68+
it "doesn't rebuild an artifact when signature is the same" do
69+
forge.forge(concept)
70+
initial_path = path_template.full_path(concept.signature)
71+
expect(File.exist?(initial_path)).to be_truthy
72+
expect(File.read(initial_path)).to eq(synthesizer.content)
73+
initial_modified_time = File.mtime(initial_path)
74+
75+
forge.forge(concept)
76+
expect(File.exist?(initial_path)).to be_truthy
77+
expect(File.read(initial_path)).to eq(synthesizer.content)
78+
expect(File.mtime(initial_path)).to eq(initial_modified_time)
2979
end
3080
end
3181
end

spec/fake_concept.rb spec/mock_concept.rb

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4-
class FakeConcept
4+
class MockConcept
55
extend T::Sig
66
include Forge::Concept
77

8+
sig { void }
9+
def initialize
10+
@dump = T.let({}, T::Hash[Symbol, T.untyped])
11+
end
12+
13+
sig { returns(T::Hash[Symbol, T.untyped]) }
14+
attr_accessor :dump
15+
816
sig { override.returns(T::Hash[Symbol, T.untyped]) }
917
def dump
10-
{}
18+
@dump
1119
end
1220

1321
sig { override.returns(Symbol) }

spec/fake_synthesizer.rb spec/mock_synthesizer.rb

+15-5
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,42 @@
22
# frozen_string_literal: true
33

44
require_relative "../lib/forge/synthesizer"
5-
require_relative "./fake_concept"
65

7-
class FakeSynthesizer
6+
class MockSynthesizer
87
extend T::Sig
98
include Forge::Synthesizer
109

1110
NAME = "Dummy Synthesizer"
12-
CONTENT = "This is a dummy synthesizer"
1311
PATH = "tmp/dummy"
1412
BASENAME = "dummy"
1513
EXTENSION = "txt"
1614

15+
sig { void }
16+
def initialize
17+
@synthesizes = T.let(true, T::Boolean)
18+
@content = T.let("This is a dummy synthesizer", String)
19+
end
20+
1721
sig { override.returns(String) }
1822
def name
1923
NAME
2024
end
2125

26+
sig { returns(T::Boolean) }
27+
attr_accessor :synthesizes
28+
29+
sig { returns(String) }
30+
attr_accessor :content
31+
2232
sig { override.params(concept: Forge::Concept).returns(T::Boolean) }
2333
def synthesizes?(concept)
24-
true
34+
synthesizes
2535
end
2636

2737
sig { override.params(concept: Forge::Concept).returns(Forge::Artifact) }
2838
def synthesize(concept)
2939
Forge::Artifact.new(
30-
content: CONTENT,
40+
content: content,
3141
path_template: path_template(concept),
3242
signature: concept.signature
3343
)

0 commit comments

Comments
 (0)