licensee 8.9.2 → 9.0.0.beta.1

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +1 -1
  3. data/bin/licensee +42 -28
  4. data/lib/licensee.rb +14 -28
  5. data/lib/licensee/content_helper.rb +38 -12
  6. data/lib/licensee/license.rb +22 -9
  7. data/lib/licensee/matchers.rb +14 -0
  8. data/lib/licensee/matchers/cabal.rb +16 -0
  9. data/lib/licensee/matchers/{copyright_matcher.rb → copyright.rb} +1 -5
  10. data/lib/licensee/matchers/{cran_matcher.rb → cran.rb} +1 -1
  11. data/lib/licensee/matchers/{dice_matcher.rb → dice.rb} +1 -7
  12. data/lib/licensee/matchers/{dist_zilla_matcher.rb → dist_zilla.rb} +1 -1
  13. data/lib/licensee/matchers/{exact_matcher.rb → exact.rb} +2 -1
  14. data/lib/licensee/matchers/gemspec.rb +24 -0
  15. data/lib/licensee/matchers/{package_matcher.rb → matcher.rb} +5 -3
  16. data/lib/licensee/matchers/{npm_bower_matcher.rb → npm_bower.rb} +2 -2
  17. data/lib/licensee/matchers/package.rb +22 -0
  18. data/lib/licensee/project_files.rb +8 -0
  19. data/lib/licensee/project_files/license_file.rb +22 -4
  20. data/lib/licensee/project_files/package_manager_file.rb +37 -0
  21. data/lib/licensee/project_files/project_file.rb +65 -0
  22. data/lib/licensee/project_files/{readme.rb → readme_file.rb} +2 -2
  23. data/lib/licensee/projects.rb +7 -0
  24. data/lib/licensee/projects/fs_project.rb +64 -27
  25. data/lib/licensee/projects/git_project.rb +49 -43
  26. data/lib/licensee/projects/project.rb +149 -0
  27. data/lib/licensee/version.rb +1 -1
  28. data/spec/bin_spec.rb +9 -2
  29. data/spec/fixtures/crlf-license/LICENSE +674 -0
  30. data/spec/fixtures/fcpl-modified-mpl/LICENSE +193 -193
  31. data/spec/fixtures/ipsum.txt +19 -0
  32. data/spec/fixtures/license-in-parent-folder/LICENSE.txt +21 -0
  33. data/spec/fixtures/license-in-parent-folder/license-folder/LICENSE.txt +21 -0
  34. data/spec/fixtures/multiple-license-files/LICENSE +362 -0
  35. data/spec/fixtures/multiple-license-files/LICENSE.txt +21 -0
  36. data/spec/integration_spec.rb +57 -22
  37. data/spec/licensee/content_helper_spec.rb +33 -9
  38. data/spec/licensee/license_spec.rb +8 -1
  39. data/spec/licensee/matchers/cabal_matcher_spec.rb +45 -0
  40. data/spec/licensee/matchers/copyright_matcher_spec.rb +1 -1
  41. data/spec/licensee/matchers/cran_matcher_spec.rb +11 -2
  42. data/spec/licensee/matchers/dice_matcher_spec.rb +1 -2
  43. data/spec/licensee/matchers/dist_zilla_matcher_spec.rb +11 -2
  44. data/spec/licensee/matchers/exact_matcher_spec.rb +1 -1
  45. data/spec/licensee/matchers/gemspec_matcher_spec.rb +19 -1
  46. data/spec/licensee/matchers/npm_bower_matcher_spec.rb +23 -6
  47. data/spec/licensee/matchers/package_matcher_spec.rb +31 -2
  48. data/spec/licensee/project_files/license_file_spec.rb +62 -6
  49. data/spec/licensee/project_files/package_info_spec.rb +10 -1
  50. data/spec/{project_file_spec.rb → licensee/project_files/project_file_spec.rb} +14 -1
  51. data/spec/licensee/project_files/readme_spec.rb +1 -1
  52. data/spec/licensee/project_spec.rb +134 -9
  53. data/spec/licensee_spec.rb +4 -3
  54. data/spec/spec_helper.rb +12 -23
  55. data/spec/vendored_license_spec.rb +16 -13
  56. metadata +29 -17
  57. data/lib/licensee/matchers/gemspec_matcher.rb +0 -19
  58. data/lib/licensee/project.rb +0 -87
  59. data/lib/licensee/project_file.rb +0 -39
  60. data/lib/licensee/project_files/package_info.rb +0 -31
@@ -11,11 +11,13 @@ RSpec.describe Licensee::ContentHelper do
11
11
  let(:content) do
12
12
  <<-EOS.freeze.gsub(/^\s*/, '')
13
13
  # The MIT License
14
- =================
14
+ =================
15
15
 
16
- Copyright 2016 Ben Balter
16
+ Copyright 2016 Ben Balter
17
+ *************************
17
18
 
18
19
  The made
20
+ * * * *
19
21
  up license.
20
22
  -----------
21
23
  EOS
@@ -31,15 +33,11 @@ EOS
31
33
  expect(subject.length).to eql(20)
32
34
  end
33
35
 
34
- it 'knows the max delta' do
35
- expect(subject.max_delta).to eql(1)
36
- end
37
-
38
36
  context 'a very long license' do
39
37
  let(:content) { 'license' * 1000 }
40
38
 
41
- it 'does not return a max delta larger than 150' do
42
- expect(subject.max_delta).to eql(150)
39
+ it 'returns the max delta' do
40
+ expect(subject.max_delta).to eql(140)
43
41
  end
44
42
  end
45
43
 
@@ -54,7 +52,19 @@ EOS
54
52
  end
55
53
 
56
54
  it 'calculates the hash' do
57
- expect(subject.hash).to eql('3c59634b9fae4396a76a978f3f6aa718ed790a9a')
55
+ content_hash = '3c59634b9fae4396a76a978f3f6aa718ed790a9a'
56
+ expect(subject.content_hash).to eql(content_hash)
57
+ end
58
+
59
+ it 'wraps' do
60
+ content = Licensee::ContentHelper.wrap(mit.content, 40)
61
+ lines = content.split("\n")
62
+ expect(lines.first.length).to be <= 40
63
+ end
64
+
65
+ it 'formats percents' do
66
+ percent = Licensee::ContentHelper.format_percent(12.3456789)
67
+ expect(percent).to eql('12.35%')
58
68
  end
59
69
 
60
70
  context 'normalizing' do
@@ -72,6 +82,19 @@ EOS
72
82
 
73
83
  it 'strips HRs' do
74
84
  expect(normalized_content).to_not match '---'
85
+ expect(normalized_content).to_not match '==='
86
+ expect(normalized_content).to_not include '***'
87
+ expect(normalized_content).to_not include '* *'
88
+ end
89
+
90
+ it 'strips formatting from the MPL' do
91
+ license = Licensee::License.find('mpl-2.0')
92
+ expect(license.content_normalized).to_not include('* *')
93
+ end
94
+
95
+ it 'wraps' do
96
+ lines = mit.content_normalized(wrap: 40).split("\n")
97
+ expect(lines.first.length).to be <= 40
75
98
  end
76
99
 
77
100
  it 'squeezes whitespace' do
@@ -80,6 +103,7 @@ EOS
80
103
 
81
104
  it 'strips whitespace' do
82
105
  expect(normalized_content).to_not match(/\n/)
106
+ expect(normalized_content).to_not match(/\t/)
83
107
  end
84
108
 
85
109
  it 'strips markdown headings' do
@@ -12,6 +12,7 @@ RSpec.describe Licensee::License do
12
12
  let(:unlicense) { described_class.find('unlicense') }
13
13
  let(:other) { described_class.find('other') }
14
14
  let(:gpl) { described_class.find('gpl-3.0') }
15
+ let(:lgpl) { described_class.find('lgpl-3.0') }
15
16
 
16
17
  let(:license_dir) do
17
18
  File.expand_path 'vendor/choosealicense.com/_licenses', project_root
@@ -169,6 +170,11 @@ RSpec.describe Licensee::License do
169
170
  expect(gpl).to be_gpl
170
171
  end
171
172
 
173
+ it 'knows a license is lgpl' do
174
+ expect(mit).to_not be_gpl
175
+ expect(lgpl).to be_lgpl
176
+ end
177
+
172
178
  it 'knows if a license is CC' do
173
179
  expect(gpl).to_not be_creative_commons
174
180
  expect(cc_by).to be_creative_commons
@@ -185,7 +191,8 @@ RSpec.describe Licensee::License do
185
191
  end
186
192
 
187
193
  it 'computes the hash' do
188
- expect(mit.hash).to eql('d64f3bb4282a97b37454b5bb96a8a264a3363dc3')
194
+ content_hash = 'd64f3bb4282a97b37454b5bb96a8a264a3363dc3'
195
+ expect(mit.content_hash).to eql(content_hash)
189
196
  end
190
197
 
191
198
  context 'with content stubbed' do
@@ -0,0 +1,45 @@
1
+ RSpec.describe Licensee::Matchers::Cabal do
2
+ let(:content) { 'license: mit' }
3
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'LICENSE.txt') }
4
+ let(:mit) { Licensee::License.find('mit') }
5
+ let(:no_license) { Licensee::License.find('no-license') }
6
+ subject { described_class.new(file) }
7
+
8
+ it 'matches' do
9
+ expect(subject.match).to eql(mit)
10
+ end
11
+
12
+ it 'has a confidence' do
13
+ expect(subject.confidence).to eql(90)
14
+ end
15
+
16
+ {
17
+ 'whitespace' => 'license : mit',
18
+ 'no whitespace' => 'license:mit',
19
+ 'leading whitespace' => ' license:mit'
20
+ }.each do |description, license_declaration|
21
+ context "with a #{description} declaration" do
22
+ let(:content) { license_declaration }
23
+
24
+ it 'matches' do
25
+ expect(subject.match).to eql(mit)
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'no license field' do
31
+ let(:content) { 'foo: bar' }
32
+
33
+ it 'returns nil' do
34
+ expect(subject.match).to be_nil
35
+ end
36
+ end
37
+
38
+ context 'an unknown license' do
39
+ let(:content) { 'license: foo' }
40
+
41
+ it 'returns other' do
42
+ expect(subject.match).to eql(Licensee::License.find('other'))
43
+ end
44
+ end
45
+ end
@@ -1,6 +1,6 @@
1
1
  RSpec.describe Licensee::Matchers::Copyright do
2
2
  let(:content) { 'Copyright 2015 Ben Balter' }
3
- let(:file) { Licensee::Project::LicenseFile.new(content, 'LICENSE.txt') }
3
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'LICENSE.txt') }
4
4
  let(:mit) { Licensee::License.find('mit') }
5
5
  let(:no_license) { Licensee::License.find('no-license') }
6
6
 
@@ -3,7 +3,7 @@ RSpec.describe Licensee::Matchers::Cran do
3
3
  let(:gpl2) { Licensee::License.find('gpl-2.0') }
4
4
  let(:gpl3) { Licensee::License.find('gpl-3.0') }
5
5
  let(:content) { "License: MIT + file LICENSE\nPackage: test" }
6
- let(:file) { Licensee::Project::LicenseFile.new(content, 'DESCRIPTION') }
6
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'DESCRIPTION') }
7
7
  subject { described_class.new(file) }
8
8
 
9
9
  it 'stores the file' do
@@ -26,7 +26,8 @@ RSpec.describe Licensee::Matchers::Cran do
26
26
  'GPL (>=2) + file LICENSE' => Licensee::License.find('gpl-2.0'),
27
27
  'GPL (>=3)' => Licensee::License.find('gpl-3.0'),
28
28
  'GPL-2' => Licensee::License.find('gpl-2.0'),
29
- 'GPL-3' => Licensee::License.find('gpl-3.0')
29
+ 'GPL-3' => Licensee::License.find('gpl-3.0'),
30
+ 'Foo' => Licensee::License.find('other')
30
31
  }.each do |license_declaration, license|
31
32
  context "with '#{license_declaration}' declaration" do
32
33
  let(:content) { "Package: test\nLicense: #{license_declaration}" }
@@ -36,4 +37,12 @@ RSpec.describe Licensee::Matchers::Cran do
36
37
  end
37
38
  end
38
39
  end
40
+
41
+ context 'with no license field' do
42
+ let(:content) { 'Package: test' }
43
+
44
+ it 'returns nil' do
45
+ expect(subject.match).to be_nil
46
+ end
47
+ end
39
48
  end
@@ -5,7 +5,7 @@ RSpec.describe Licensee::Matchers::Dice do
5
5
  let(:cc_by) { Licensee::License.find('cc-by-4.0') }
6
6
  let(:cc_by_sa) { Licensee::License.find('cc-by-sa-4.0') }
7
7
  let(:content) { sub_copyright_info(gpl.content) }
8
- let(:file) { Licensee::Project::LicenseFile.new(content, 'LICENSE.txt') }
8
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'LICENSE.txt') }
9
9
  subject { described_class.new(file) }
10
10
 
11
11
  it 'stores the file' do
@@ -50,7 +50,6 @@ RSpec.describe Licensee::Matchers::Dice do
50
50
  end
51
51
 
52
52
  it "doesn't match" do
53
- skip 'Stacked MIT + GPL not properly detected'
54
53
  expect(content).to_not be_detected_as(gpl)
55
54
  expect(subject.match).to eql(nil)
56
55
  expect(subject.matches).to be_empty
@@ -1,7 +1,7 @@
1
1
  RSpec.describe Licensee::Matchers::DistZilla do
2
2
  let(:mit) { Licensee::License.find('mit') }
3
3
  let(:content) { 'license = MIT' }
4
- let(:file) { Licensee::Project::LicenseFile.new(content, 'dist.ini') }
4
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'dist.ini') }
5
5
  subject { described_class.new(file) }
6
6
 
7
7
  it 'stores the file' do
@@ -18,7 +18,8 @@ RSpec.describe Licensee::Matchers::DistZilla do
18
18
 
19
19
  {
20
20
  'spdx name' => ['license = MIT', 'mit'],
21
- 'non spdx name' => ['license = Mozilla_2_0', 'mpl-2.0']
21
+ 'non spdx name' => ['license = Mozilla_2_0', 'mpl-2.0'],
22
+ 'other license' => ['license = Foo', 'other']
22
23
  }.each do |description, license_declaration_and_key|
23
24
  context "with a #{description}" do
24
25
  let(:content) { license_declaration_and_key[0] }
@@ -29,4 +30,12 @@ RSpec.describe Licensee::Matchers::DistZilla do
29
30
  end
30
31
  end
31
32
  end
33
+
34
+ context 'no license field' do
35
+ let(:content) { 'foo = bar' }
36
+
37
+ it 'returns nil' do
38
+ expect(subject.match).to be_nil
39
+ end
40
+ end
32
41
  end
@@ -1,7 +1,7 @@
1
1
  RSpec.describe Licensee::Matchers::Exact do
2
2
  let(:mit) { Licensee::License.find('mit') }
3
3
  let(:content) { sub_copyright_info(mit.content) }
4
- let(:file) { Licensee::Project::LicenseFile.new(content, 'LICENSE.txt') }
4
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'LICENSE.txt') }
5
5
  subject { described_class.new(file) }
6
6
 
7
7
  it 'stores the file' do
@@ -1,7 +1,9 @@
1
1
  RSpec.describe Licensee::Matchers::Gemspec do
2
2
  let(:mit) { Licensee::License.find('mit') }
3
3
  let(:content) { "s.license = 'mit'" }
4
- let(:file) { Licensee::Project::LicenseFile.new(content, 'project.gemspec') }
4
+ let(:file) do
5
+ Licensee::ProjectFiles::LicenseFile.new(content, 'project.gemspec')
6
+ end
5
7
  subject { described_class.new(file) }
6
8
 
7
9
  it 'matches' do
@@ -26,4 +28,20 @@ RSpec.describe Licensee::Matchers::Gemspec do
26
28
  end
27
29
  end
28
30
  end
31
+
32
+ context 'no license field' do
33
+ let(:content) { "s.foo = 'bar'" }
34
+
35
+ it 'returns nil' do
36
+ expect(subject.match).to be_nil
37
+ end
38
+ end
39
+
40
+ context 'an unknown license' do
41
+ let(:content) { "s.license = 'foo'" }
42
+
43
+ it 'returns other' do
44
+ expect(subject.match).to eql(Licensee::License.find('other'))
45
+ end
46
+ end
29
47
  end
@@ -1,6 +1,6 @@
1
1
  RSpec.describe Licensee::Matchers::NpmBower do
2
2
  let(:content) { '"license": "mit"' }
3
- let(:file) { Licensee::Project::LicenseFile.new(content, 'LICENSE.txt') }
3
+ let(:file) { Licensee::ProjectFiles::LicenseFile.new(content, 'LICENSE.txt') }
4
4
  let(:mit) { Licensee::License.find('mit') }
5
5
  let(:no_license) { Licensee::License.find('no-license') }
6
6
  subject { described_class.new(file) }
@@ -14,11 +14,12 @@ RSpec.describe Licensee::Matchers::NpmBower do
14
14
  end
15
15
 
16
16
  {
17
- 'double quotes' => '"license": "mit"',
18
- 'single quotes' => "'license': 'mit'",
19
- 'mixed quotes' => "'license': \"mit\"",
20
- 'whitespace' => "'license' : 'mit'",
21
- 'no whitespace' => "'license':'mit'"
17
+ 'double quotes' => '"license": "mit"',
18
+ 'single quotes' => "'license': 'mit'",
19
+ 'mixed quotes' => "'license': \"mit\"",
20
+ 'whitespace' => "'license' : 'mit'",
21
+ 'no whitespace' => "'license':'mit'",
22
+ 'leading whitespace' => " 'license':'mit'"
22
23
  }.each do |description, license_declaration|
23
24
  context "with a #{description} declaration" do
24
25
  let(:content) { license_declaration }
@@ -28,4 +29,20 @@ RSpec.describe Licensee::Matchers::NpmBower do
28
29
  end
29
30
  end
30
31
  end
32
+
33
+ context 'no license field' do
34
+ let(:content) { 'foo: bar' }
35
+
36
+ it 'returns nil' do
37
+ expect(subject.match).to be_nil
38
+ end
39
+ end
40
+
41
+ context 'an unknown license' do
42
+ let(:content) { "'license': 'foo'" }
43
+
44
+ it 'returns other' do
45
+ expect(subject.match).to eql(Licensee::License.find('other'))
46
+ end
47
+ end
31
48
  end
@@ -1,9 +1,14 @@
1
1
  RSpec.describe Licensee::Matchers::Package do
2
2
  let(:mit) { Licensee::License.find('mit') }
3
3
  let(:content) { '' }
4
- let(:file) { Licensee::Project::LicenseFile.new(content, 'project.gemspec') }
4
+ let(:file) do
5
+ Licensee::ProjectFiles::LicenseFile.new(content, 'project.gemspec')
6
+ end
7
+ let(:license_property) { 'mit' }
5
8
  subject { described_class.new(file) }
6
- before { allow(subject).to receive(:license_property).and_return('mit') }
9
+ before do
10
+ allow(subject).to receive(:license_property).and_return(license_property)
11
+ end
7
12
 
8
13
  it 'matches' do
9
14
  expect(subject.match).to eql(mit)
@@ -12,4 +17,28 @@ RSpec.describe Licensee::Matchers::Package do
12
17
  it 'has confidence' do
13
18
  expect(subject.confidence).to eql(90)
14
19
  end
20
+
21
+ context 'with a nil license property' do
22
+ let(:license_property) { nil }
23
+
24
+ it 'matches to nil' do
25
+ expect(subject.match).to be_nil
26
+ end
27
+ end
28
+
29
+ context 'with an empty license property' do
30
+ let(:license_property) { '' }
31
+
32
+ it 'matches to nil' do
33
+ expect(subject.match).to be_nil
34
+ end
35
+ end
36
+
37
+ context 'with an unmatched license proprerty' do
38
+ let(:license_property) { 'foo' }
39
+
40
+ it 'matches to other' do
41
+ expect(subject.match).to eql(Licensee::License.find('other'))
42
+ end
43
+ end
15
44
  end
@@ -1,4 +1,4 @@
1
- RSpec.describe Licensee::Project::LicenseFile do
1
+ RSpec.describe Licensee::ProjectFiles::LicenseFile do
2
2
  let(:filename) { 'LICENSE.txt' }
3
3
  let(:mit) { Licensee::License.find('mit') }
4
4
  let(:content) { sub_copyright_info(mit.content) }
@@ -31,7 +31,8 @@ RSpec.describe Licensee::Project::LicenseFile do
31
31
  end
32
32
 
33
33
  it 'creates the hash' do
34
- expect(subject.hash).to eql('d64f3bb4282a97b37454b5bb96a8a264a3363dc3')
34
+ content_hash = 'd64f3bb4282a97b37454b5bb96a8a264a3363dc3'
35
+ expect(subject.content_hash).to eql(content_hash)
35
36
  end
36
37
 
37
38
  context 'filename scoring' do
@@ -52,10 +53,10 @@ RSpec.describe Licensee::Project::LicenseFile do
52
53
  'copying.image' => 0.5,
53
54
  'COPYRIGHT.go' => 0.5,
54
55
  'LICENSE-MIT' => 0.4,
55
- 'MIT-LICENSE.txt' => 0.4,
56
+ 'MIT-LICENSE.txt' => 0.3,
56
57
  'mit-license-foo.md' => 0.4,
57
- 'COPYING-GPL' => 0.3,
58
- 'COPYRIGHT-BSD' => 0.3,
58
+ 'COPYING-GPL' => 0.35,
59
+ 'COPYRIGHT-BSD' => 0.35,
59
60
  'OFL.md' => 0.2,
60
61
  'ofl.textile' => 0.1,
61
62
  'ofl' => 0.05,
@@ -133,7 +134,7 @@ RSpec.describe Licensee::Project::LicenseFile do
133
134
  end
134
135
 
135
136
  context 'CC false positives' do
136
- let(:regex) { Licensee::Project::LicenseFile::CC_FALSE_POSITIVE_REGEX }
137
+ let(:regex) { Licensee::ProjectFiles::LicenseFile::CC_FALSE_POSITIVE_REGEX }
137
138
 
138
139
  it "knows MIT isn't a potential false positive" do
139
140
  expect(subject.content).to_not match(regex)
@@ -182,4 +183,59 @@ Creative Commons Attribution-NonCommercial 4.0
182
183
  end
183
184
  end
184
185
  end
186
+
187
+ context 'LGPL' do
188
+ let(:lgpl) { Licensee::License.find('lgpl-3.0') }
189
+ let(:content) { sub_copyright_info(lgpl.content) }
190
+
191
+ context 'with a COPYING.lesser file' do
192
+ let(:filename) { 'COPYING.lesser' }
193
+
194
+ it 'knows when a license file is LGPL' do
195
+ expect(subject).to be_lgpl
196
+ end
197
+
198
+ context 'with non-lgpl content' do
199
+ let(:content) { sub_copyright_info(mit.content) }
200
+
201
+ it 'is not lgpl' do
202
+ expect(subject).to_not be_lgpl
203
+ end
204
+ end
205
+ end
206
+
207
+ context 'with a different file name' do
208
+ let(:filename) { 'COPYING' }
209
+
210
+ it 'is not lgpl' do
211
+ expect(subject).to_not be_lgpl
212
+ end
213
+ end
214
+ end
215
+
216
+ context 'GPL' do
217
+ let(:gpl) { Licensee::License.find('gpl-3.0') }
218
+ let(:content) { sub_copyright_info(gpl.content) }
219
+
220
+ it 'knows its GPL' do
221
+ expect(subject).to be_gpl
222
+ end
223
+
224
+ context 'another license' do
225
+ let(:content) { sub_copyright_info(mit.content) }
226
+
227
+ it 'is not GPL' do
228
+ expect(subject).to_not be_gpl
229
+ end
230
+ end
231
+ end
232
+
233
+ context 'an unknown license' do
234
+ let(:content) { 'foo' }
235
+ let(:other) { Licensee::License.find('other') }
236
+
237
+ it 'matches to other' do
238
+ expect(subject.license).to eql(other)
239
+ end
240
+ end
185
241
  end