hglib 0.1.pre20180129173049 → 0.1.0

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.
@@ -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
+