hglib 0.1.pre20180129173049 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'hglib/repo/id'
6
+
7
+
8
+ RSpec.describe Hglib::Repo::Id do
9
+
10
+ it "can be created for an empty repo" do
11
+ result = described_class.new( '000000000000', 'tip' )
12
+
13
+ expect( result.global ).to eq( '000000000000' )
14
+ expect( result ).to eq( '000000000000' )
15
+ expect( result.tags ).to contain_exactly( 'tip' )
16
+ expect( result.bookmarks ).to be_empty
17
+ expect( result ).to_not have_uncommitted_changes
18
+ end
19
+
20
+
21
+ it "can be created for a repo with commits" do
22
+ result = described_class.new( 'd03a659966ec', 'tip' )
23
+
24
+ expect( result.global ).to eq( 'd03a659966ec' )
25
+ expect( result ).to eq( 'd03a659966ec' )
26
+ expect( result.tags ).to contain_exactly( 'tip' )
27
+ expect( result.bookmarks ).to be_empty
28
+ expect( result ).to_not have_uncommitted_changes
29
+ end
30
+
31
+
32
+ it "can be created for a repo with uncommitted changes" do
33
+ result = described_class.new( 'd03a659966ec', 'tip', uncommitted_changes: true )
34
+
35
+ expect( result.global ).to eq( 'd03a659966ec' )
36
+ expect( result ).to have_uncommitted_changes
37
+ end
38
+
39
+
40
+ it "can be created for a repo with more than one tag" do
41
+ result = described_class.new( 'd03a659966ec', 'qbase', 'qtip', 'repo-features.patch', 'tip' )
42
+
43
+ expect( result.global ).to eq( 'd03a659966ec' )
44
+ expect( result.tags ).to contain_exactly( 'qbase', 'qtip', 'repo-features.patch', 'tip' )
45
+ end
46
+
47
+
48
+ it "can be created for a repo with a bookmark" do
49
+ result = described_class.new( 'd03a659966ec', 'tip', bookmarks: 'master' )
50
+
51
+ expect( result.global ).to eq( 'd03a659966ec' )
52
+ expect( result.bookmarks ).to contain_exactly( 'master' )
53
+ end
54
+
55
+
56
+ it "can be created for a repo with more than one bookmark" do
57
+ result = described_class.new( 'd03a659966ec', 'tip', bookmarks: ['master', 'github/master'] )
58
+
59
+ expect( result.global ).to eq( 'd03a659966ec' )
60
+ expect( result.bookmarks ).to contain_exactly( 'master', 'github/master' )
61
+ end
62
+
63
+
64
+ describe "equality" do
65
+
66
+ it "is equal to an object of the same class with the same values" do
67
+ id = described_class.new( 'd03a659966ec',
68
+ 'qbase', 'qtip', 'repo-features.patch', 'tip',
69
+ uncommitted_changes: true,
70
+ bookmarks: ['master', 'live']
71
+ )
72
+
73
+ copy = id.dup
74
+
75
+ expect( id ).to eq( copy )
76
+ end
77
+
78
+
79
+ it "is equal to the String that contains the same revision identifier" do
80
+ id = described_class.new( 'd03a659966ec',
81
+ 'qbase', 'qtip', 'repo-features.patch', 'tip',
82
+ uncommitted_changes: true,
83
+ bookmarks: ['master', 'live']
84
+ )
85
+
86
+ expect( id ).to eq( 'd03a659966ec' )
87
+ end
88
+
89
+ end
90
+
91
+
92
+ describe "parsing server output" do
93
+
94
+ it "can parse the server output from an empty repo" do
95
+ result = described_class.parse( '000000000000 tip' )
96
+
97
+ expect( result.global ).to eq( '000000000000' )
98
+ expect( result ).to eq( '000000000000' )
99
+ expect( result.tags ).to contain_exactly( 'tip' )
100
+ expect( result.bookmarks ).to be_empty
101
+ expect( result ).to_not have_uncommitted_changes
102
+ end
103
+
104
+
105
+ it "can be parsed from the server output from a repo with commits" do
106
+ result = described_class.parse( 'd03a659966ec tip' )
107
+
108
+ expect( result.global ).to eq( 'd03a659966ec' )
109
+ expect( result ).to eq( 'd03a659966ec' )
110
+ expect( result.tags ).to contain_exactly( 'tip' )
111
+ expect( result.bookmarks ).to be_empty
112
+ expect( result ).to_not have_uncommitted_changes
113
+ end
114
+
115
+
116
+ it "can be parsed from the server output from a repo with uncommitted changes" do
117
+ result = described_class.parse( 'd03a659966ec+ tip' )
118
+
119
+ expect( result.global ).to eq( 'd03a659966ec' )
120
+ expect( result ).to have_uncommitted_changes
121
+ end
122
+
123
+
124
+ it "can be parsed from the server output from a repo with more than one tag" do
125
+ result = described_class.parse( 'd03a659966ec qbase/qtip/repo-features.patch/tip' )
126
+
127
+ expect( result.global ).to eq( 'd03a659966ec' )
128
+ expect( result.tags ).to contain_exactly( 'qbase', 'qtip', 'repo-features.patch', 'tip' )
129
+ end
130
+
131
+
132
+ it "can be parsed from the server output from a repo with a bookmark" do
133
+ result = described_class.parse( 'd03a659966ec tip master' )
134
+
135
+ expect( result.global ).to eq( 'd03a659966ec' )
136
+ expect( result.bookmarks ).to contain_exactly( 'master' )
137
+ end
138
+
139
+
140
+ it "can be parsed from the server output from a repo with more than one bookmark" do
141
+ result = described_class.parse( 'd03a659966ec tip master/servant' )
142
+
143
+ expect( result.global ).to eq( 'd03a659966ec' )
144
+ expect( result.bookmarks ).to contain_exactly( 'master', 'servant' )
145
+ end
146
+
147
+ end
148
+
149
+
150
+ describe "stringifying" do
151
+
152
+ it "works for the ID of an empty repo" do
153
+ id = described_class.new( '000000000000', 'tip' )
154
+
155
+ expect( id.to_s ).to eq( '000000000000 tip' )
156
+ end
157
+
158
+
159
+ it "works for the ID of a repo with uncommitted changes" do
160
+ id = described_class.new( 'd03a659966ec', 'tip', uncommitted_changes: true )
161
+
162
+ expect( id.to_s ).to eq( 'd03a659966ec+ tip' )
163
+ end
164
+
165
+
166
+ it "works for the ID of a repo with more than one tag" do
167
+ id = described_class.new( 'd03a659966ec', 'qbase', 'qtip', 'repo-features.patch', 'tip' )
168
+
169
+ expect( id.to_s ).to eq( 'd03a659966ec qbase/qtip/repo-features.patch/tip' )
170
+ end
171
+
172
+
173
+ it "works for the ID of a repo with a bookmark" do
174
+ id = described_class.new( 'd03a659966ec', 'tip', bookmarks: 'master' )
175
+
176
+ expect( id.to_s ).to eq( 'd03a659966ec tip master' )
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env rspec -cfd
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../spec_helper'
5
+
6
+ require 'hglib/repo/log_entry'
7
+
8
+
9
+ RSpec.describe Hglib::Repo::LogEntry do
10
+
11
+ RAW_LOG_ENTRY = {
12
+ "bookmarks" => ['master'],
13
+ "branch" => "default",
14
+ "date" => [1526420446, 25200],
15
+ "desc" => "Flesh out the features of Repo objects",
16
+ "node" => "d4af915821dea2feca29288dc16742c0d41cee8c",
17
+ "parents" => ["a366819bd05b8dd995440105340e057528be25e6"],
18
+ "phase" => "public",
19
+ "rev" => 5,
20
+ "tags" => ['github/master', 'tip'],
21
+ "user" => "Michael Granger <ged@FaerieMUD.org>"
22
+ }.freeze
23
+
24
+ VERBOSE_LOG_ENTRY = RAW_LOG_ENTRY.merge(
25
+ "files" => %w[.hoerc .ruby-version lib/hglib/repo.rb spec/hglib/repo_spec.rb]
26
+ ).freeze
27
+
28
+
29
+ it "can be created from the JSON log hash" do
30
+ entry = described_class.new( RAW_LOG_ENTRY )
31
+
32
+ expected_time = Time.parse( 'Tue May 15 14:40:46 2018 -0700' )
33
+
34
+ expect( entry ).to be_a( described_class )
35
+ expect( entry.changeset ).to eq( '5:d4af915821de' )
36
+ expect( entry.rev ).to eq( 5 )
37
+ expect( entry.node ).to eq( 'd4af915821dea2feca29288dc16742c0d41cee8c' )
38
+ expect( entry.bookmarks ).to contain_exactly( 'master' )
39
+ expect( entry.tags ).to contain_exactly( 'github/master', 'tip' )
40
+ expect( entry.user ).to eq( 'Michael Granger <ged@FaerieMUD.org>' )
41
+ expect( entry.date ).to be_a( Time ).and( eq(expected_time) )
42
+ expect( entry.summary ).to eq( 'Flesh out the features of Repo objects' )
43
+
44
+ expect( entry.diff ).to be_nil
45
+ expect( entry.files ).to be_empty
46
+ end
47
+
48
+
49
+ it "can be created from verbose log entry JSON" do
50
+ entry = described_class.new( VERBOSE_LOG_ENTRY )
51
+
52
+ expected_time = Time.parse( 'Tue May 15 14:40:46 2018 -0700' )
53
+
54
+ expect( entry ).to be_a( described_class )
55
+ expect( entry.changeset ).to eq( '5:d4af915821de' )
56
+ expect( entry.rev ).to eq( 5 )
57
+ expect( entry.node ).to eq( 'd4af915821dea2feca29288dc16742c0d41cee8c' )
58
+ expect( entry.bookmarks ).to contain_exactly( 'master' )
59
+ expect( entry.tags ).to contain_exactly( 'github/master', 'tip' )
60
+ expect( entry.user ).to eq( 'Michael Granger <ged@FaerieMUD.org>' )
61
+ expect( entry.date ).to be_a( Time ).and( eq(expected_time) )
62
+ expect( entry.summary ).to eq( 'Flesh out the features of Repo objects' )
63
+
64
+ expect( entry.diff ).to be_nil
65
+ expect( entry.files ).to contain_exactly( *VERBOSE_LOG_ENTRY['files'] )
66
+ end
67
+
68
+ end
69
+
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'hglib/repo'
6
+
7
+
8
+ RSpec.describe Hglib::Repo do
9
+
10
+ let( :repo_dir ) do
11
+ Dir.mktmpdir( ['hglib', 'repodir'] )
12
+ end
13
+
14
+ let( :server ) { instance_double(Hglib::Server) }
15
+
16
+
17
+ before( :each ) do
18
+ allow( Hglib::Server ).to receive( :new ).and_return( server )
19
+ end
20
+
21
+
22
+ it "can fetch the status of the working directory" do
23
+ repo = described_class.new( repo_dir )
24
+
25
+ expect( server ).to receive( :run ).with( :status, {} ).
26
+ and_return([
27
+ "M ",
28
+ ".gems\n",
29
+ "M ",
30
+ "lib/hglib/repo.rb\n",
31
+ "M ",
32
+ "lib/hglib/server.rb\n",
33
+ "? ",
34
+ "coverage/assets/0.10.2/magnify.png\n"
35
+ ])
36
+
37
+ result = repo.status
38
+
39
+ expect( result ).to be_a( Hash )
40
+ expect( result ).to include(
41
+ Pathname('.gems') => 'M',
42
+ Pathname('lib/hglib/repo.rb') => 'M',
43
+ Pathname('lib/hglib/server.rb') => 'M',
44
+ Pathname('coverage/assets/0.10.2/magnify.png') => '?'
45
+ )
46
+ end
47
+
48
+
49
+ it "can fetch the identification of the repository's current revision" do
50
+ repo = described_class.new( repo_dir )
51
+
52
+ expect( server ).to receive( :run ).with( :id, {} ).
53
+ and_return( ["80d775fc1d2c+ qbase/qtip/repo-features.patch/tip master\n"] )
54
+
55
+ result = repo.id
56
+
57
+ expect( result ).to be_a( Hglib::Repo::Id ).and( eq '80d775fc1d2c' )
58
+ expect( result.tags ).to eq( %w[qbase qtip repo-features.patch tip] )
59
+ expect( result.bookmarks ).to eq( %w[master] )
60
+ end
61
+
62
+
63
+ it "can fetch the log of the repository" do
64
+ repo = described_class.new( repo_dir )
65
+
66
+ expect( server ).to receive( :run ).with( :log, {T: 'json', graph: false} ).
67
+ and_return([
68
+ "[",
69
+ "\n {\n \"bookmarks\": [],\n \"branch\": \"default\",\n \"date\": " +
70
+ "[1516812073, 28800],\n \"desc\": \"Make ruby-version less specific\"," +
71
+ "\n \"node\": \"81f357f730d9f22d560e4bd2790e7cf5aa5b7ec7\",\n \"parents\":" +
72
+ " [\"d6c97f99b012199d9088e85bb0940147446c6a87\"],\n \"phase\": \"public\",\n " +
73
+ " \"rev\": 1,\n \"tags\": [],\n \"user\": \"Michael Granger" +
74
+ " <ged@FaerieMUD.org>\"\n }",
75
+ ",",
76
+ "\n {\n",
77
+ " \"bookmarks\": []",
78
+ ",\n",
79
+ " \"branch\": \"default\"",
80
+ ",\n",
81
+ " \"date\": [1516811121, 28800]",
82
+ ",\n",
83
+ " \"desc\": \"Initial commit.\"",
84
+ ",\n",
85
+ " \"node\": \"d6c97f99b012199d9088e85bb0940147446c6a87\"",
86
+ ",\n",
87
+ " \"parents\": [\"0000000000000000000000000000000000000000\"]",
88
+ ",\n",
89
+ " \"phase\": \"public\"",
90
+ ",\n",
91
+ " \"rev\": 0",
92
+ ",\n",
93
+ " \"tags\": []",
94
+ ",\n",
95
+ " \"user\": \"Michael Granger <ged@FaerieMUD.org>\"",
96
+ "\n }",
97
+ "\n]\n"
98
+ ])
99
+
100
+ result = repo.log
101
+
102
+ expect( result ).to be_an( Array ).and( all be_a(Hglib::Repo::LogEntry) )
103
+ end
104
+
105
+ end
106
+
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'tmpdir'
6
+ require 'hglib/server'
7
+
8
+
9
+ RSpec.describe Hglib::Server do
10
+
11
+ def cmdserver_message( channel, data='' )
12
+ return [ channel, data.bytesize, data ].pack( 'A*I>A*' )
13
+ end
14
+
15
+ def cmdserver_header( channel, bytes )
16
+ return [ channel, bytes ].pack( 'aI>' )
17
+ end
18
+
19
+
20
+ let( :repo_dir ) do
21
+ Dir.mktmpdir( ['hglib', 'repodir'] )
22
+ end
23
+
24
+ let( :parent_reader ) { StringIO.new }
25
+ let( :child_reader ) { instance_double(IO, close: true) }
26
+ let( :parent_writer ) { StringIO.new }
27
+ let( :child_writer ) { instance_double(IO, close: true) }
28
+
29
+ let( :hello_message ) do
30
+ cmdserver_message( 'o', "capabilities: runcommand getencoding\nencoding: UTF-8\n" )
31
+ end
32
+ let( :line_input_prompt ) do
33
+ cmdserver_header( 'L', 4096 )
34
+ end
35
+ let( :byte_input_prompt ) do
36
+ cmdserver_header( 'I', 4096 )
37
+ end
38
+ let( :result_message ) do
39
+ cmdserver_message( 'r', "done" )
40
+ end
41
+
42
+
43
+ before( :each ) do
44
+ allow( Process ).to receive( :spawn ).and_return( 1111 )
45
+ allow( Process ).to receive( :kill ).with( :TERM, 1111 )
46
+ allow( Process ).to receive( :wait ).with( 1111, Process::WNOHANG )
47
+
48
+ allow( IO ).to receive( :pipe ).and_return(
49
+ [parent_reader, child_writer],
50
+ [child_reader, parent_writer]
51
+ )
52
+ end
53
+
54
+
55
+ describe "option-mangling" do
56
+
57
+ it "mangles single-letter keys into one-hyphen options" do
58
+ result = described_class.mangle_options( C: true, p: true, g: true )
59
+ expect( result ).to eq( %w[-C -p -g] )
60
+ end
61
+
62
+
63
+ it "mangles multi-letter keys into two-hyphen options" do
64
+ result = described_class.mangle_options( copies: true, patch: true )
65
+ expect( result ).to eq( %w[--copies --patch] )
66
+ end
67
+
68
+
69
+ it "drops single-letter keys with falsey values" do
70
+ result = described_class.mangle_options( g: false, p: nil )
71
+ expect( result ).to eq( [] )
72
+ end
73
+
74
+
75
+ it "negates multi-letter keys with falsey values" do
76
+ result = described_class.mangle_options( graph: false, patch: nil )
77
+ expect( result ).to eq( %w[--no-graph --no-patch] )
78
+ end
79
+
80
+
81
+ it "appends String values onto options with a space for single-letter keys" do
82
+ result = described_class.mangle_options( P: '165' )
83
+ expect( result ).to eq( %w[-P 165] )
84
+ end
85
+
86
+
87
+ it "appends String values onto options with an equal for multi-letter keys" do
88
+ result = described_class.mangle_options( prune: '165' )
89
+ expect( result ).to eq( %w[--prune=165] )
90
+ end
91
+
92
+ end
93
+
94
+
95
+ it "knows whether or not it has been started" do
96
+ parent_reader.write( hello_message )
97
+ parent_reader.rewind
98
+
99
+ server = described_class.new( repo_dir )
100
+
101
+ expect( server ).to_not be_started
102
+ server.start
103
+ expect( server ).to be_started
104
+ end
105
+
106
+
107
+ it "calls the on_line_input callback when the command server asks for line input" do
108
+ parent_reader.write( hello_message )
109
+ parent_reader.write( line_input_prompt )
110
+ parent_reader.write( result_message )
111
+ parent_reader.rewind
112
+
113
+ server = described_class.new( repo_dir )
114
+ server.on_line_input do |max_bytes|
115
+ "a line of input no more than #{max_bytes} long"
116
+ end
117
+ server.run( :record )
118
+
119
+ expect( parent_writer.string ).to include(
120
+ "runcommand\n",
121
+ "record",
122
+ "a line of input no more than 4096 long\n"
123
+ )
124
+ end
125
+
126
+
127
+ it "calls the on_byte_input callback when the command server asks for byte input" do
128
+ parent_reader.write( hello_message )
129
+ parent_reader.write( byte_input_prompt )
130
+ parent_reader.write( result_message )
131
+ parent_reader.rewind
132
+
133
+ server = described_class.new( repo_dir )
134
+ server.on_byte_input do |max_bytes|
135
+ "bytes of input no more than #{max_bytes} long"
136
+ end
137
+ server.run( :import, '-' )
138
+
139
+ expect( parent_writer.string ).to include(
140
+ "runcommand\n",
141
+ "import\0-",
142
+ "bytes of input no more than 4096 long"
143
+ )
144
+ end
145
+
146
+
147
+ it "can be stopped manually" do
148
+ parent_reader.write( hello_message )
149
+ parent_reader.rewind
150
+
151
+ server = described_class.new( repo_dir )
152
+ server.start
153
+
154
+ server.stop
155
+ expect( parent_writer.string ).to be_empty
156
+ end
157
+
158
+
159
+ it "doesn't error when told to stop if it hasn't been started" do
160
+ server = described_class.new( repo_dir )
161
+ expect( server.stop ).to be_falsey
162
+ end
163
+
164
+
165
+ end
166
+