macos.rb 0.50.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.
- checksums.yaml +7 -0
- data/CHANGELOG +572 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +129 -0
- data/Rakefile +9 -0
- data/lib/Array/all_but_last.rb +7 -0
- data/lib/MacOS/Apple80211.rb +68 -0
- data/lib/MacOS/Applications.rb +73 -0
- data/lib/MacOS/Defaults/ComAppleDock.rb +153 -0
- data/lib/MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents.rb +109 -0
- data/lib/MacOS/Defaults.rb +126 -0
- data/lib/MacOS/DiskUtil.rb +63 -0
- data/lib/MacOS/Dmg.rb +99 -0
- data/lib/MacOS/HardwarePort.rb +79 -0
- data/lib/MacOS/IfConfig.rb +183 -0
- data/lib/MacOS/Keychain.rb +26 -0
- data/lib/MacOS/LaunchControl.rb +36 -0
- data/lib/MacOS/SystemProfiler.rb +119 -0
- data/lib/MacOS/USBFlashDrive.rb +26 -0
- data/lib/MacOS/Users.rb +16 -0
- data/lib/MacOS/VERSION.rb +6 -0
- data/lib/MacOS/VPN.rb +13 -0
- data/lib/MacOS/Voice.rb +40 -0
- data/lib/MacOS.rb +27 -0
- data/lib/Module/alias_methods.rb +7 -0
- data/lib/Ordinal/Array.rb +8 -0
- data/lib/Ordinal.rb +157 -0
- data/lib/Thoran/Array/AllButLast/all_but_last.rb +28 -0
- data/lib/Thoran/Array/LastX/lastX.rb +33 -0
- data/lib/Thoran/Module/AliasMethods/alias_methods.rb +23 -0
- data/lib/Thoran/String/Capture/capture.rb +28 -0
- data/lib/Version.rb +381 -0
- data/macos.rb.gemspec +59 -0
- data/test/MacOS/Apple80211_test.rb +80 -0
- data/test/MacOS/Applications_test.rb +82 -0
- data/test/MacOS/Defaults/ComAppleDock_test.rb +66 -0
- data/test/MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents_test.rb +67 -0
- data/test/MacOS/Defaults_test.rb +144 -0
- data/test/MacOS/DiskUtil_test.rb +48 -0
- data/test/MacOS/Dmg_test.rb +69 -0
- data/test/MacOS/HardwarePort_test.rb +46 -0
- data/test/MacOS/IfConfig_test.rb +144 -0
- data/test/MacOS/Keychain_test.rb +22 -0
- data/test/MacOS/LaunchControl_test.rb +40 -0
- data/test/MacOS/SystemProfiler_test.rb +69 -0
- data/test/MacOS/USBFlashDrive_test.rb +20 -0
- data/test/MacOS/Users_test.rb +10 -0
- data/test/MacOS/VPN_test.rb +18 -0
- data/test/MacOS/Voice_test.rb +46 -0
- data/test/MacOS_test.rb +17 -0
- data/test/VERSION_test.rb +19 -0
- data/test/commands_test.rb +50 -0
- data/test/fixtures/airport-I-deprecated.txt +2 -0
- data/test/fixtures/airport-I.txt +17 -0
- data/test/fixtures/defaults-domains.txt +1 -0
- data/test/fixtures/defaults-export-com.apple.dock.xml +47 -0
- data/test/fixtures/defaults-read-NSUserKeyEquivalents.txt +4 -0
- data/test/fixtures/df-k.txt +6 -0
- data/test/fixtures/diskutil-list-plist.xml +104 -0
- data/test/fixtures/dscl-list-users.txt +8 -0
- data/test/fixtures/ifconfig-a.txt +53 -0
- data/test/fixtures/networksetup-listallhardwareports.txt +39 -0
- data/test/fixtures/say-v.txt +8 -0
- data/test/fixtures/system_profiler-SPAirPortDataType.txt +44 -0
- data/test/fixtures/system_profiler-SPSoftwareDataType.txt +14 -0
- data/test/fixtures/system_profiler-SPUSBDataType.txt +58 -0
- data/test/gemspec_test.rb +50 -0
- data/test/helper.rb +18 -0
- metadata +183 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative '../../../helper'
|
|
2
|
+
require 'MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents'
|
|
3
|
+
|
|
4
|
+
describe MacOS::Defaults::NSGlobalDomain::NSUserKeyEquivalents do
|
|
5
|
+
let(:keys){MacOS::Defaults::NSGlobalDomain::NSUserKeyEquivalents}
|
|
6
|
+
|
|
7
|
+
describe ".key_code" do
|
|
8
|
+
it "codes modifier and arrow keys, in either case" do
|
|
9
|
+
_(keys.key_code(%w{cmd alt left})).must_equal('@~\\UF702')
|
|
10
|
+
_(keys.key_code(%w{CMD ALT LEFT})).must_equal('@~\\UF702')
|
|
11
|
+
_(keys.key_code(:ctrl, :shift, :right)).must_equal('^$\\UF703')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "keeps a letter as itself" do
|
|
15
|
+
_(keys.key_code(%w{cmd D})).must_equal('@d')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "raises for a key it doesn't know" do
|
|
19
|
+
_{keys.key_code(%w{cmd hyper})}.must_raise(RuntimeError)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe ".parse_key_code" do
|
|
24
|
+
it "splits a key code at each \\U" do
|
|
25
|
+
_(keys.parse_key_code('@~\UF702')).must_equal(['@~', 'F702'])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "assigning keys" do
|
|
30
|
+
it "makes a defaults write command for each menu item" do
|
|
31
|
+
_(keys.assign_key_code_command_strings('@~\\UF702', ['Previous Tab', 'Show Previous Tab'])).must_equal([
|
|
32
|
+
"defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Previous Tab' '@~\\UF702'",
|
|
33
|
+
"defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Show Previous Tab' '@~\\UF702'",
|
|
34
|
+
])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "runs them, succeeding where each does" do
|
|
38
|
+
result = nil
|
|
39
|
+
commands = commands_run_by(keys){result = keys.assign_keys(%w{cmd alt left}, 'Previous Tab')}
|
|
40
|
+
_(result).must_equal(true)
|
|
41
|
+
_(commands).must_equal([["defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Previous Tab' '@~\\UF702'"]])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "loads several shortcuts at once" do
|
|
45
|
+
shortcuts = [{keys: %w{cmd alt left}, menu_items: ['Previous Tab']}, {keys: %w{cmd shift d}, menu_items: ['Find Previous']}]
|
|
46
|
+
commands = commands_run_by(keys){keys.load(shortcuts)}
|
|
47
|
+
_(commands.size).must_equal(2)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe ".read_keys" do
|
|
52
|
+
it "reads the shortcuts back, as keys and the menu items they're for" do
|
|
53
|
+
keys.stub(:`, fixture('defaults-read-NSUserKeyEquivalents.txt')) do
|
|
54
|
+
_(keys.read_keys).must_equal([
|
|
55
|
+
{keys: [:cmd, :alt, :left], menu_items: ['Previous Tab']},
|
|
56
|
+
{keys: [:cmd, :shift, 'd'], menu_items: ['Find Previous']},
|
|
57
|
+
])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "writes them as YAML" do
|
|
62
|
+
keys.stub(:`, fixture('defaults-read-NSUserKeyEquivalents.txt')) do
|
|
63
|
+
_(YAML.load(keys.to_yaml, permitted_classes: [Symbol]).first[:menu_items]).must_equal(['Previous Tab'])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
require 'MacOS/Defaults'
|
|
4
|
+
|
|
5
|
+
describe MacOS::Defaults do
|
|
6
|
+
def with_domains(&block)
|
|
7
|
+
MacOS::Defaults.stub(:`, fixture('defaults-domains.txt'), &block)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Stands in for MacOS::Defaults.new, making Defaults whose named methods only record the calls made on them.
|
|
11
|
+
def recorder(calls, *methods)
|
|
12
|
+
->(domain:) do
|
|
13
|
+
defaults = MacOS::Defaults.allocate
|
|
14
|
+
defaults.send(:initialize, domain: domain)
|
|
15
|
+
methods.each{|method| defaults.define_singleton_method(method){|*args| calls << [domain, method, *args]}}
|
|
16
|
+
defaults
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe ".domains" do
|
|
21
|
+
it "lists every domain" do
|
|
22
|
+
with_domains do
|
|
23
|
+
_(MacOS::Defaults.domains).must_equal(%w{com.apple.dock com.apple.finder com.apple.Safari})
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "makes a Defaults for each" do
|
|
28
|
+
with_domains do
|
|
29
|
+
_(MacOS::Defaults.all.map{|defaults| defaults.instance_variable_get(:@domain)}).must_equal(%w{com.apple.dock com.apple.finder com.apple.Safari})
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "a domain" do
|
|
35
|
+
it "is valid where defaults lists it, or where it's the global domain" do
|
|
36
|
+
with_domains do
|
|
37
|
+
_(MacOS::Defaults.new(domain: 'com.apple.dock').domain_valid?).must_equal(true)
|
|
38
|
+
_(MacOS::Defaults.new(domain: '-globalDomain').domain_valid?).must_equal(true)
|
|
39
|
+
_(MacOS::Defaults.new(domain: 'com.example.nowhere').domain_valid?).must_equal(false)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "knows whether it's the global domain" do
|
|
44
|
+
_(MacOS::Defaults.new(domain: '-globalDomain').global_domain?).must_equal(true)
|
|
45
|
+
_(MacOS::Defaults.new(domain: 'com.apple.dock').global_domain?).must_equal(false)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "exporting and importing" do
|
|
50
|
+
it "exports a valid domain to a plist file" do
|
|
51
|
+
commands = []
|
|
52
|
+
MacOS::Defaults.stub(:domains, %w{com.apple.dock}) do
|
|
53
|
+
defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
|
|
54
|
+
defaults.stub(:`, ->(command){commands << command; ''}){_(defaults.export).must_equal(true)}
|
|
55
|
+
end
|
|
56
|
+
_(commands).must_equal(['defaults export com.apple.dock /tmp/com.apple.dock.plist'])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "won't export a domain defaults doesn't know" do
|
|
60
|
+
MacOS::Defaults.stub(:domains, []) do
|
|
61
|
+
_{MacOS::Defaults.new(domain: 'com.example.nowhere').export}.must_raise(RuntimeError)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "won't import a domain without its plist file" do
|
|
66
|
+
_{MacOS::Defaults.new(domain: 'com.example.nowhere').import}.must_raise(RuntimeError)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "exports and imports every domain in bulk" do
|
|
70
|
+
calls = []
|
|
71
|
+
MacOS::Defaults.stub(:domains, %w{com.apple.dock com.apple.finder}) do
|
|
72
|
+
MacOS::Defaults.stub(:new, recorder(calls, :export, :import)) do
|
|
73
|
+
MacOS::Defaults.bulk_export_to_plist_files
|
|
74
|
+
MacOS::Defaults.bulk_import_from_plist_files
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
_(calls).must_equal([['com.apple.dock', :export], ['com.apple.finder', :export], ['com.apple.dock', :import], ['com.apple.finder', :import]])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "from YAML" do
|
|
82
|
+
let(:attributes){[{'autohide' => true}, {'tilesize' => 48}, {'scale' => 0.5}, {'orientation' => 'bottom'}, {'persistent-apps' => ['/Applications/Example.app']}]}
|
|
83
|
+
|
|
84
|
+
it "writes each attribute, typed by its value" do
|
|
85
|
+
defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
|
|
86
|
+
commands = commands_run_by(defaults){_(defaults.from_yaml(attributes)).must_equal(true)}
|
|
87
|
+
_(commands).must_equal([
|
|
88
|
+
%w{defaults write com.apple.dock autohide -bool true},
|
|
89
|
+
%w{defaults write com.apple.dock tilesize -int 48},
|
|
90
|
+
%w{defaults write com.apple.dock scale -float 0.5},
|
|
91
|
+
%w{defaults write com.apple.dock orientation -string bottom},
|
|
92
|
+
%w{defaults write com.apple.dock persistent-apps -array /Applications/Example.app},
|
|
93
|
+
])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "is false where a write fails" do
|
|
97
|
+
defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
|
|
98
|
+
defaults.stub(:system, false) do
|
|
99
|
+
_(defaults.from_yaml([{'autohide' => true}])).must_equal(false)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "writes a Hash as a dictionary" do
|
|
104
|
+
defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
|
|
105
|
+
commands = commands_run_by(defaults){defaults.from_yaml([{'example-dict' => {'size' => 48, 'shown' => true}}])}
|
|
106
|
+
_(commands).must_equal([%w{defaults write com.apple.dock example-dict -dict size 48 shown true}])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "reads what to_yaml writes" do
|
|
110
|
+
written = MacOS::Defaults.new(domain: 'com.apple.dock')
|
|
111
|
+
yaml = written.stub(:to_h, {'autohide' => true, 'tilesize' => 48}){written.to_yaml}
|
|
112
|
+
_(YAML.safe_load(yaml)['com.apple.dock']).must_equal([{'autohide' => true}, {'tilesize' => 48}])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "writes each domain a YAML file gives" do
|
|
116
|
+
Dir.mktmpdir do |directory|
|
|
117
|
+
path = File.join(directory, 'defaults.yml')
|
|
118
|
+
File.write(path, {'com.apple.dock' => [{'autohide' => true}], 'com.apple.finder' => [{'ShowPathbar' => true}]}.to_yaml)
|
|
119
|
+
calls = []
|
|
120
|
+
MacOS::Defaults.stub(:new, recorder(calls, :from_yaml)){MacOS::Defaults.from_yaml(path)}
|
|
121
|
+
_(calls).must_equal([['com.apple.dock', :from_yaml, [{'autohide' => true}]], ['com.apple.finder', :from_yaml, [{'ShowPathbar' => true}]]])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "reads DEFAULT_YAML_FILENAME where it's given no file" do
|
|
126
|
+
loaded = []
|
|
127
|
+
YAML.stub(:load_file, ->(path){loaded << path; {}}){MacOS::Defaults.from_yaml}
|
|
128
|
+
_(loaded).must_equal([MacOS::Defaults::DEFAULT_YAML_FILENAME])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "#to_h" do
|
|
133
|
+
it "reads the domain's exported plist file" do
|
|
134
|
+
domain = "com.example.macos-rb-test-#{Process.pid}"
|
|
135
|
+
path = File.join(MacOS::Defaults::DEFAULT_PLIST_DIRECTORY, "#{domain}.plist")
|
|
136
|
+
plist = CFPropertyList::List.new
|
|
137
|
+
plist.value = CFPropertyList.guess('autohide' => true, 'tilesize' => 48)
|
|
138
|
+
plist.save(path, CFPropertyList::List::FORMAT_XML)
|
|
139
|
+
_(MacOS::Defaults.new(domain: domain).to_h).must_equal('autohide' => true, 'tilesize' => 48)
|
|
140
|
+
ensure
|
|
141
|
+
File.delete(path) if path && File.exist?(path)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/DiskUtil'
|
|
3
|
+
|
|
4
|
+
describe MacOS::DiskUtil do
|
|
5
|
+
def with_disks(&block)
|
|
6
|
+
MacOS::DiskUtil.stub(:xml_volume_plist, fixture('diskutil-list-plist.xml'), &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def with_df(&block)
|
|
10
|
+
MacOS::DiskUtil.stub(:`, fixture('df-k.txt'), &block)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "the volumes" do
|
|
14
|
+
it "keeps the disks with partitions" do
|
|
15
|
+
with_disks do
|
|
16
|
+
_(MacOS::DiskUtil.volumes_and_partitions.map{|disk| disk['DeviceIdentifier']}).must_equal(%w{disk0 disk4})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "gives each partition's device, mount point, name and size" do
|
|
21
|
+
with_disks do
|
|
22
|
+
_(MacOS::DiskUtil.volumes.last).must_equal(device_id: 'disk4s1', mount_point: '/Volumes/EXAMPLE', name: 'EXAMPLE', size: 32006733824)
|
|
23
|
+
_(MacOS::DiskUtil.volumes.map{|volume| volume[:device_id]}).must_equal(%w{disk0s1 disk0s2 disk4s1})
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "gives those which are mounted as its external volumes" do
|
|
28
|
+
with_disks do
|
|
29
|
+
_(MacOS::DiskUtil.external_volumes.map{|volume| volume[:name]}).must_equal(%w{EXAMPLE})
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "the free space" do
|
|
35
|
+
it "gives the bytes available on a device or a volume" do
|
|
36
|
+
with_df do
|
|
37
|
+
_(MacOS::DiskUtil.free_bytes_for_device_id('disk4s1')).must_equal(31241024 * 1024)
|
|
38
|
+
_(MacOS::DiskUtil.free_bytes_for_volume_name('EXAMPLE')).must_equal(31241024 * 1024)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "gives nil for a device which isn't there" do
|
|
43
|
+
with_df do
|
|
44
|
+
_(MacOS::DiskUtil.free_bytes_for_device_id('disk9s9')).must_be_nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/Dmg'
|
|
3
|
+
|
|
4
|
+
describe MacOS::Dmg do
|
|
5
|
+
def run_quietly(&block)
|
|
6
|
+
Dir.stub(:chdir, nil){commands_run_by(MacOS::Dmg, &block)}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe "the class" do
|
|
10
|
+
it "mounts, installs from and unmounts a disk image, escaping each name for the shell" do
|
|
11
|
+
commands = run_quietly do
|
|
12
|
+
MacOS::Dmg.mount('Example App.dmg')
|
|
13
|
+
MacOS::Dmg.run_installer(mount_point: 'Example App', pkg_name: 'Example App.pkg')
|
|
14
|
+
MacOS::Dmg.unmount('Example App')
|
|
15
|
+
end
|
|
16
|
+
_(commands).must_equal([
|
|
17
|
+
['hdiutil attach Example\\ App.dmg'],
|
|
18
|
+
["sudo installer -pkg Example\\ App.pkg -target '/'"],
|
|
19
|
+
['hdiutil detach -Force /Volumes/Example\\ App'],
|
|
20
|
+
])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "installs from a disk image it's given the names of" do
|
|
24
|
+
commands = run_quietly{MacOS::Dmg.install(path_to_dmg: '/tmp/Example-1.0.dmg', disk_image_name: 'Example-1.0.dmg', package: 'Example')}
|
|
25
|
+
_(commands).must_equal([
|
|
26
|
+
['hdiutil attach /tmp/Example-1.0.dmg'],
|
|
27
|
+
["sudo installer -pkg Example.pkg -target '/'"],
|
|
28
|
+
['hdiutil detach -Force /Volumes/Example-1'],
|
|
29
|
+
])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "prints each command where verbose" do
|
|
33
|
+
MacOS::Dmg.verbose = true
|
|
34
|
+
_{run_quietly{MacOS::Dmg.mount('Example.dmg')}}.must_output("hdiutil attach Example.dmg\n")
|
|
35
|
+
ensure
|
|
36
|
+
MacOS::Dmg.verbose = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "an instance" do
|
|
41
|
+
let(:dmg){MacOS::Dmg.new(path_to_dmg: '/tmp/Example-1.0.dmg', disk_image_name: 'Example-1.0.dmg', package: 'Example')}
|
|
42
|
+
|
|
43
|
+
it "takes its package's name and its mount point from its other names, where they aren't given" do
|
|
44
|
+
_(dmg.pkg_name).must_equal('Example.pkg')
|
|
45
|
+
_(dmg.mount_point).must_equal('Example-1')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "takes them as given" do
|
|
49
|
+
dmg.pkg_name = 'Other.pkg'
|
|
50
|
+
dmg.mount_point = 'Other'
|
|
51
|
+
_(dmg.pkg_name).must_equal('Other.pkg')
|
|
52
|
+
_(dmg.mount_point).must_equal('Other')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "answers to each of its disk image's names" do
|
|
56
|
+
dmg.filename = 'Renamed.dmg'
|
|
57
|
+
_([dmg.disk_image_name, dmg.filename, dmg.dmg_filename, dmg.image_name]).must_equal(['Renamed.dmg'] * 4)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "mounts, installs and unmounts" do
|
|
61
|
+
commands = run_quietly{dmg.install}
|
|
62
|
+
_(commands).must_equal([
|
|
63
|
+
['hdiutil attach /tmp/Example-1.0.dmg'],
|
|
64
|
+
["sudo installer -pkg Example.pkg -target '/'"],
|
|
65
|
+
['hdiutil detach -Force /Volumes/Example-1'],
|
|
66
|
+
])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/HardwarePort'
|
|
3
|
+
|
|
4
|
+
describe MacOS::HardwarePort do
|
|
5
|
+
def with_ports(&block)
|
|
6
|
+
MacOS::HardwarePort.stub(:output, fixture('networksetup-listallhardwareports.txt'), &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe ".parse" do
|
|
10
|
+
it "makes one for each hardware port networksetup lists" do
|
|
11
|
+
ports = MacOS::HardwarePort.parse(fixture('networksetup-listallhardwareports.txt'))
|
|
12
|
+
_(ports.map(&:interface)).must_equal(%w{en4 en5 en6 en8 bridge0 en0 en1 en2 en3})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "reads each port's name, device and Ethernet address" do
|
|
16
|
+
wifi = MacOS::HardwarePort.parse(fixture('networksetup-listallhardwareports.txt')).find{|port| port.device == 'en0'}
|
|
17
|
+
_(wifi.hardware_port).must_equal('Wi-Fi')
|
|
18
|
+
_(wifi.name).must_equal('Wi-Fi')
|
|
19
|
+
_(wifi.ethernet_address).must_equal('00:00:5e:00:53:08')
|
|
20
|
+
_(wifi.mac_address).must_equal('00:00:5e:00:53:08')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "finding" do
|
|
25
|
+
it "finds a port by its interface, its name or its MAC address" do
|
|
26
|
+
with_ports do
|
|
27
|
+
_(MacOS::HardwarePort.find_by_interface('en0').name).must_equal('Wi-Fi')
|
|
28
|
+
_(MacOS::HardwarePort.find_by_device('bridge0').name).must_equal('Thunderbolt Bridge')
|
|
29
|
+
_(MacOS::HardwarePort.find_by_name('Wi-Fi').interface).must_equal('en0')
|
|
30
|
+
_(MacOS::HardwarePort.find_by_hardware_port('Thunderbolt 2').interface).must_equal('en2')
|
|
31
|
+
_(MacOS::HardwarePort.find_by_mac_address('00:00:5e:00:53:04').interface).must_equal('en4')
|
|
32
|
+
_(MacOS::HardwarePort.find_by_ethernet_address('00:00:5e:00:53:04').interface).must_equal('en4')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "finds nothing for a port which isn't there" do
|
|
37
|
+
with_ports do
|
|
38
|
+
_(MacOS::HardwarePort.find_by_name('Nowhere')).must_be_nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "keeps the raw output to itself" do
|
|
44
|
+
_(MacOS::HardwarePort.respond_to?(:output)).must_equal(false)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/IfConfig'
|
|
3
|
+
|
|
4
|
+
describe MacOS::IfConfig do
|
|
5
|
+
let(:output){fixture('ifconfig-a.txt')}
|
|
6
|
+
|
|
7
|
+
def with_ifconfig(output = fixture('ifconfig-a.txt'), &block)
|
|
8
|
+
MacOS::IfConfig.stub(:output, output, &block)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def interface(name)
|
|
12
|
+
MacOS::IfConfig.parse(output).find{|ifconfig| ifconfig.interface == name}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe ".parse" do
|
|
16
|
+
it "makes one for each interface, in the order ifconfig lists them" do
|
|
17
|
+
_(MacOS::IfConfig.parse(output).map(&:interface)).must_equal(%w{lo0 gif0 stf0 en1 en0 awdl0 bridge0 utun0 utun4})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "reads an interface's address, netmask and broadcast address" do
|
|
21
|
+
en0 = interface('en0')
|
|
22
|
+
_(en0.ipv4_address).must_equal('192.0.2.114')
|
|
23
|
+
_(en0.netmask).must_equal('0xffffff00')
|
|
24
|
+
_(en0.broadcast_address).must_equal('192.0.2.255')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "reads an address without a broadcast address" do
|
|
28
|
+
_(interface('lo0').ipv4_address).must_equal('127.0.0.1')
|
|
29
|
+
_(interface('lo0').netmask).must_equal('0xff000000')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "reads the near end of a point-to-point address" do
|
|
33
|
+
_(interface('utun4').ipv4_address).must_equal('198.51.100.250')
|
|
34
|
+
_(interface('utun4').netmask).must_equal('0xffffffff')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "reads the MAC address, status and flags" do
|
|
38
|
+
en0 = interface('en0')
|
|
39
|
+
_(en0.mac_address).must_equal('00:00:5e:00:53:02')
|
|
40
|
+
_(en0.status).must_equal('active')
|
|
41
|
+
_(en0.flags).must_include('UP')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "reads lines with a trailing space, as older ifconfigs give them" do
|
|
45
|
+
older = "en0: flags=8863<UP,BROADCAST> mtu 1500\n\tether 00:00:5e:00:53:02 \n\tinet 192.0.2.114 netmask 0xffffff00 \n"
|
|
46
|
+
en0 = MacOS::IfConfig.parse(older).first
|
|
47
|
+
_(en0.mac_address).must_equal('00:00:5e:00:53:02')
|
|
48
|
+
_(en0.ipv4_address).must_equal('192.0.2.114')
|
|
49
|
+
_(en0.netmask).must_equal('0xffffff00')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "gives the same each time" do
|
|
53
|
+
first = MacOS::IfConfig.parse(output).map(&:interface)
|
|
54
|
+
_(MacOS::IfConfig.parse(output).map(&:interface)).must_equal(first)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "gives none for output with no interfaces" do
|
|
58
|
+
_(MacOS::IfConfig.parse('')).must_equal([])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "the interfaces in each state" do
|
|
63
|
+
it "lists those which are active and inactive" do
|
|
64
|
+
with_ifconfig do
|
|
65
|
+
_(MacOS::IfConfig.active_interfaces_names).must_equal(%w{en0 awdl0})
|
|
66
|
+
_(MacOS::IfConfig.inactive_interfaces_names).must_equal(%w{en1 bridge0})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "lists those which are up: active or flagged UP, and with an address" do
|
|
71
|
+
with_ifconfig do
|
|
72
|
+
_(MacOS::IfConfig.up_interfaces_names).must_equal(%w{lo0 en0 utun4})
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe "the interfaces themselves" do
|
|
78
|
+
it "lists every interface's name, and those active and inactive" do
|
|
79
|
+
with_ifconfig do
|
|
80
|
+
_(MacOS::IfConfig.interface_names).must_equal(%w{lo0 gif0 stf0 en1 en0 awdl0 bridge0 utun0 utun4})
|
|
81
|
+
_(MacOS::IfConfig.active_interfaces.map(&:name)).must_equal(%w{en0 awdl0})
|
|
82
|
+
_(MacOS::IfConfig.inactive_interfaces.map(&:name)).must_equal(%w{en1 bridge0})
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "the addresses" do
|
|
88
|
+
it "lists every MAC address and IPv4 address" do
|
|
89
|
+
with_ifconfig do
|
|
90
|
+
_(MacOS::IfConfig.mac_addresses).must_equal(%w{00:00:5e:00:53:01 00:00:5e:00:53:02 00:00:5e:00:53:03 00:00:5e:00:53:01})
|
|
91
|
+
_(MacOS::IfConfig.ipv4_addresses).must_equal(%w{127.0.0.1 192.0.2.114 198.51.100.250})
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "finding" do
|
|
97
|
+
it "finds an interface by its name, MAC address or IPv4 address" do
|
|
98
|
+
with_ifconfig do
|
|
99
|
+
_(MacOS::IfConfig.find_by_interface('en0').mac_address).must_equal('00:00:5e:00:53:02')
|
|
100
|
+
_(MacOS::IfConfig.find_by_mac_address('00:00:5e:00:53:03').interface).must_equal('awdl0')
|
|
101
|
+
_(MacOS::IfConfig.find_by_ipv4_address('198.51.100.250').interface).must_equal('utun4')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "matches names against a Regexp, or a String taken literally" do
|
|
106
|
+
with_ifconfig do
|
|
107
|
+
_(MacOS::IfConfig.all_matching(/\Autun\d/).map(&:name)).must_equal(%w{utun0 utun4})
|
|
108
|
+
_(MacOS::IfConfig.all_matching('en').map(&:name)).must_equal(%w{en1 en0})
|
|
109
|
+
_(MacOS::IfConfig.all_matching('.')).must_equal([])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe "bringing interfaces down" do
|
|
115
|
+
it "brings down only those matching which are up" do
|
|
116
|
+
with_ifconfig do
|
|
117
|
+
commands = commands_run_by(MacOS::IfConfig) do
|
|
118
|
+
_(MacOS::IfConfig.bring_down_matching_interfaces(/\Autun/)).must_equal(%w{utun4})
|
|
119
|
+
end
|
|
120
|
+
_(commands).must_equal([%w{sudo ifconfig utun4 down}])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "destroys an interface" do
|
|
125
|
+
commands = commands_run_by(MacOS::IfConfig){MacOS::IfConfig.destroy_interface('utun4')}
|
|
126
|
+
_(commands).must_equal([%w{sudo ifconfig utun4 destroy}])
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "an interface" do
|
|
131
|
+
it "gives its MAC address as its Ethernet address too" do
|
|
132
|
+
_(interface('en0').ethernet_address).must_equal('00:00:5e:00:53:02')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "knows whether it's virtual" do
|
|
136
|
+
_(interface('utun4').virtual?).wont_be_nil
|
|
137
|
+
_(interface('en0').virtual?).must_be_nil
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "keeps the raw output to itself" do
|
|
142
|
+
_(MacOS::IfConfig.respond_to?(:output)).must_equal(false)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/Keychain'
|
|
3
|
+
|
|
4
|
+
describe MacOS::Keychain do
|
|
5
|
+
it "asks security for a generic password, the key escaped for the shell" do
|
|
6
|
+
_(MacOS::Keychain.find_generic_password_command('Example Net')).must_equal('security find-generic-password -ga Example\\ Net')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "gives a Wi-Fi network's password, which security writes to standard error" do
|
|
10
|
+
found = ["keychain: \"/Library/Keychains/System.keychain\"\n", "password: \"example-passphrase\"\n", nil]
|
|
11
|
+
MacOS::Keychain.stub(:find_generic_password, found) do
|
|
12
|
+
_(MacOS::Keychain.wifi_password('ExampleNet')).must_equal('example-passphrase')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "gives nil where the keychain has no password for the network" do
|
|
17
|
+
missing = ['', "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\n", nil]
|
|
18
|
+
MacOS::Keychain.stub(:find_generic_password, missing) do
|
|
19
|
+
_(MacOS::Keychain.wifi_password('Nowhere')).must_be_nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/LaunchControl'
|
|
3
|
+
|
|
4
|
+
describe MacOS::LaunchControl do
|
|
5
|
+
let(:plist){'/Library/LaunchDaemons/com.example.agent.plist'}
|
|
6
|
+
|
|
7
|
+
it "loads and unloads a plist" do
|
|
8
|
+
commands = commands_run_by(MacOS::LaunchControl) do
|
|
9
|
+
MacOS::LaunchControl.load(plist)
|
|
10
|
+
MacOS::LaunchControl.unload(plist)
|
|
11
|
+
end
|
|
12
|
+
_(commands).must_equal([["launchctl load -w #{plist}"], ["launchctl unload -w #{plist}"]])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "escapes the plist's path for the shell" do
|
|
16
|
+
commands = commands_run_by(MacOS::LaunchControl){MacOS::LaunchControl.load('/Library/Launch Agents/com.example.agent.plist')}
|
|
17
|
+
_(commands).must_equal([['launchctl load -w /Library/Launch\ Agents/com.example.agent.plist']])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "an instance" do
|
|
21
|
+
it "holds the plist it's made with" do
|
|
22
|
+
_(MacOS::LaunchControl.new(plist).path_to_plist).must_equal(plist)
|
|
23
|
+
_(MacOS::LaunchControl.new.path_to_plist).must_be_nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "loads and unloads its own plist" do
|
|
27
|
+
launch_control = MacOS::LaunchControl.new(plist)
|
|
28
|
+
commands = commands_run_by(MacOS::LaunchControl) do
|
|
29
|
+
launch_control.load
|
|
30
|
+
launch_control.unload
|
|
31
|
+
end
|
|
32
|
+
_(commands).must_equal([["launchctl load -w #{plist}"], ["launchctl unload -w #{plist}"]])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "unloads another plist where it's given one" do
|
|
36
|
+
commands = commands_run_by(MacOS::LaunchControl){MacOS::LaunchControl.new.unload('/tmp/other.plist')}
|
|
37
|
+
_(commands).must_equal([['launchctl unload -w /tmp/other.plist']])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/SystemProfiler'
|
|
3
|
+
|
|
4
|
+
describe MacOS::SystemProfiler do
|
|
5
|
+
def with_output(output = fixture('system_profiler-SPSoftwareDataType.txt'), &block)
|
|
6
|
+
MacOS::SystemProfiler.stub(:system_profiler_output, output, &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe "the system's version" do
|
|
10
|
+
it "reads it from the System Version field" do
|
|
11
|
+
with_output do
|
|
12
|
+
_(MacOS::SystemProfiler.system_version_data).must_equal('macOS 15.8.1 (24H32)')
|
|
13
|
+
_(MacOS::SystemProfiler.system_version_string).must_equal('15.8.1')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "reads the version of an older OS X" do
|
|
18
|
+
with_output(" System Version: Mac OS X 10.9.5 (13F34)\n") do
|
|
19
|
+
_(MacOS::SystemProfiler.system_version_string).must_equal('10.9.5')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "gives it as a Version, and its major, minor and tiny parts" do
|
|
24
|
+
with_output do
|
|
25
|
+
_(MacOS::SystemProfiler.system_version).must_be_instance_of(Version)
|
|
26
|
+
_(MacOS::SystemProfiler.system_version.to_s).must_equal('15.8.1')
|
|
27
|
+
_(MacOS::SystemProfiler.major_system_version).must_equal('15')
|
|
28
|
+
_(MacOS::SystemProfiler.minor_system_version).must_equal('8')
|
|
29
|
+
_(MacOS::SystemProfiler.tiny_system_version).must_equal('1')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "the kernel" do
|
|
35
|
+
it "reads its name and version" do
|
|
36
|
+
with_output do
|
|
37
|
+
_(MacOS::SystemProfiler.kernel_version_data).must_equal('Darwin 24.6.0')
|
|
38
|
+
_(MacOS::SystemProfiler.kernel_name).must_equal('Darwin')
|
|
39
|
+
_(MacOS::SystemProfiler.kernel_version_string).must_equal('24.6.0')
|
|
40
|
+
_(MacOS::SystemProfiler.kernel_version.to_s).must_equal('24.6.0')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "the other fields" do
|
|
46
|
+
it "reads each, by its method and by its aliases" do
|
|
47
|
+
with_output do
|
|
48
|
+
sp = MacOS::SystemProfiler
|
|
49
|
+
_([sp.boot_volume_data, sp.boot_volume_string]).must_equal(['Macintosh HD'] * 2)
|
|
50
|
+
_([sp.boot_mode_data, sp.boot_mode_string, sp.boot_mode]).must_equal(['Normal'] * 3)
|
|
51
|
+
_([sp.computer_name_data, sp.computer_name_string, sp.computer_name]).must_equal(['Example Mac'] * 3)
|
|
52
|
+
_([sp.username_data, sp.username_string, sp.username]).must_equal(['Example User (example)'] * 3)
|
|
53
|
+
_([sp.secure_virtual_memory_data, sp.secure_virtual_memory_string, sp.secure_virtual_memory]).must_equal(['Enabled'] * 3)
|
|
54
|
+
_([sp.system_integrity_protection_data, sp.system_integrity_protection_string, sp.system_integrity_protection]).must_equal(['Enabled'] * 3)
|
|
55
|
+
_([sp.time_since_boot_data, sp.time_since_boot_string, sp.time_since_boot]).must_equal(['6 days, 2 hours, 34 minutes'] * 3)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "gives nil for a field which isn't there" do
|
|
60
|
+
with_output('') do
|
|
61
|
+
_(MacOS::SystemProfiler.boot_mode).must_be_nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "keeps its reading of a field to itself" do
|
|
67
|
+
_(MacOS::SystemProfiler.respond_to?(:field)).must_equal(false)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative '../helper'
|
|
2
|
+
require 'MacOS/USBFlashDrive'
|
|
3
|
+
|
|
4
|
+
describe MacOS::USBFlashDrive do
|
|
5
|
+
it "asks system_profiler for the USB devices" do
|
|
6
|
+
_(MacOS::USBFlashDrive.system_profiler_usb_data_type_command).must_equal('system_profiler SPUSBDataType')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "gives every flash drive's mount point" do
|
|
10
|
+
MacOS::USBFlashDrive.stub(:raw_system_profiler_output, [fixture('system_profiler-SPUSBDataType.txt'), '', nil]) do
|
|
11
|
+
_(MacOS::USBFlashDrive.all_mount_points).must_equal(['/Volumes/EXAMPLE', '/Volumes/Example 2'])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "gives none where no flash drive is mounted" do
|
|
16
|
+
MacOS::USBFlashDrive.stub(:raw_system_profiler_output, ["USB:\n", '', nil]) do
|
|
17
|
+
_(MacOS::USBFlashDrive.all_mount_points).must_equal([])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|