- 删掉 MaxMind .mmdb 读取器、本地数据库控制器与「IP 归属地数据库」设置区块,去掉「归属地数据源」选择及 ipapi.is / DB-IP / ipinfo.io 解析;PublicAddressLookup 只向 cleanip.io 查询,结果按 IP 记一小时,收到 429 当天不再请求 - 设置同步文档不再带 geoSource / geoIncludeCity / geoAutoUpdate;公网 IP 缓存键升到第 4 版,旧数据源的缓存不再读取 - 网络详情:IP 地址区块标题不再放 cleanip.io 字标,只留在纯净度区块;纯净度色带改为直角 - 删除 server/geoip 的 GeoLite2 同步脚本;README、架构说明、第三方声明、官网与隐私页、译文同步,清掉 53 条不再使用的翻译
226 lines
10 KiB
Swift
226 lines
10 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import Metrics
|
||
@testable import SMC
|
||
|
||
@Suite("SMC 编解码")
|
||
struct SMCDecoderTests {
|
||
@Test func keyRoundTrip() {
|
||
#expect(SMCKey("F0Ac").description == "F0Ac")
|
||
#expect(SMCKey("#KEY").code == 0x234B_4559)
|
||
}
|
||
|
||
@Test func decodesCommonTypes() {
|
||
let float = withUnsafeBytes(of: Float32(1464.5)) { Array($0) }
|
||
#expect(SMCDecoder.decode(type: "flt ", bytes: float) == 1464.5)
|
||
#expect(SMCDecoder.decode(type: "ui8 ", bytes: [2]) == 2)
|
||
#expect(SMCDecoder.decode(type: "ui16", bytes: [0x01, 0x02]) == 258)
|
||
#expect(SMCDecoder.decode(type: "sp78", bytes: [0x2A, 0x80]) == 42.5)
|
||
#expect(SMCDecoder.decode(type: "fpe2", bytes: [0x16, 0xE0]) == 1464)
|
||
#expect(SMCDecoder.decode(type: "????", bytes: [1, 2]) == nil)
|
||
}
|
||
|
||
@Test func encodeMatchesDecode() throws {
|
||
for type in ["flt ", "fpe2"] {
|
||
let bytes = try #require(SMCDecoder.encode(type: type, value: 2400))
|
||
#expect(SMCDecoder.decode(type: type, bytes: bytes) == 2400)
|
||
}
|
||
}
|
||
|
||
@Test func SMCStructMatchesKernelLayout() {
|
||
#expect(MemoryLayout<SMCParam>.stride == 80)
|
||
}
|
||
|
||
@Test func temperatureGrouping() {
|
||
#expect(TemperatureCatalog.group(for: "Tp0C") == .cpu)
|
||
#expect(TemperatureCatalog.group(for: "Te05") == .cpu)
|
||
#expect(TemperatureCatalog.group(for: "Tg1U") == .gpu)
|
||
#expect(TemperatureCatalog.group(for: "TB0T") == .battery)
|
||
#expect(TemperatureCatalog.group(for: "Ts0P") == .palmRest)
|
||
#expect(TemperatureCatalog.group(for: "TC0P") == .cpu)
|
||
#expect(TemperatureCatalog.group(for: "F0Ac") == nil)
|
||
#expect(TemperatureCatalog.group(for: "TaLP") == nil)
|
||
}
|
||
}
|
||
|
||
@Suite("格式化")
|
||
struct FormatTests {
|
||
@Test func bytes() {
|
||
#expect(Format.bytes(UInt64(512)) == "512 B")
|
||
#expect(Format.bytes(UInt64(68_719_476_736)) == "64.0 GB")
|
||
#expect(Format.bytes(UInt64(1_000_000_000_000), base: .decimal) == "1.0 TB")
|
||
#expect(Format.bytes(UInt64(412_000_000_000), base: .decimal) == "412 GB")
|
||
}
|
||
|
||
@Test func menuBarRate() {
|
||
#expect(Format.menuBarRate(0) == "0 B/s")
|
||
#expect(Format.menuBarRate(512) == "512 B/s")
|
||
#expect(Format.menuBarRate(12 * 1024) == "12 KB/s")
|
||
#expect(Format.menuBarRate(154 * 1024) == "154 KB/s")
|
||
#expect(Format.menuBarRate(1000 * 1024) == "1.0 MB/s")
|
||
#expect(Format.menuBarRate(1.2 * 1024 * 1024) == "1.2 MB/s")
|
||
#expect(Format.menuBarRate(12 * 1024 * 1024) == "12 MB/s")
|
||
#expect(Format.menuBarRate(1.1 * 1024 * 1024 * 1024) == "1.1 GB/s")
|
||
}
|
||
|
||
@Test func percentAndDuration() {
|
||
#expect(Format.percent(0.257) == "26%")
|
||
#expect(Format.percent(1.4) == "100%")
|
||
#expect(Format.duration(minutes: 45) == "45 分钟")
|
||
#expect(Format.duration(minutes: 120) == "2 小时")
|
||
#expect(Format.duration(minutes: 90) == "1 小时 30 分钟")
|
||
#expect(Format.temperature(68.4) == "68°C")
|
||
#expect(Format.temperature(100, fahrenheit: true) == "212°F")
|
||
}
|
||
|
||
@Test func historyDropsOldest() {
|
||
var history = History<Int>(capacity: 3)
|
||
(1...5).forEach { history.append($0) }
|
||
#expect(history.elements == [3, 4, 5])
|
||
}
|
||
}
|
||
|
||
@Suite("本机采样", .serialized)
|
||
struct LiveSamplerTests {
|
||
@Test func cpuNeedsTwoSamples() async throws {
|
||
var sampler = CPUSampler()
|
||
#expect(sampler.sample() == nil)
|
||
try await Task.sleep(for: .milliseconds(200))
|
||
let sample = sampler.sample()
|
||
let load = try #require(sample)
|
||
#expect((0...1).contains(load.total))
|
||
#expect(load.perCore.count == CPUSampler.topology().logicalCores)
|
||
}
|
||
|
||
@Test func topologyCoversAllCores() {
|
||
let topology = CPUSampler.topology()
|
||
let indices = topology.clusters.flatMap(\.coreIndices).sorted()
|
||
#expect(indices == Array(0..<topology.logicalCores))
|
||
}
|
||
|
||
@Test func memoryIsWithinPhysicalLimits() throws {
|
||
let memory = try #require(MemorySampler().sample())
|
||
#expect(memory.total == ProcessInfo.processInfo.physicalMemory)
|
||
#expect(memory.used <= memory.total)
|
||
}
|
||
|
||
@Test func networkRateIsNonNegative() async throws {
|
||
var sampler = NetworkSampler()
|
||
_ = sampler.sample()
|
||
try await Task.sleep(for: .milliseconds(200))
|
||
let sample = sampler.sample()
|
||
let rate = try #require(sample)
|
||
#expect(rate.downloadBytesPerSecond >= 0)
|
||
#expect(rate.uploadBytesPerSecond >= 0)
|
||
}
|
||
|
||
@Test func processesIncludeCurrentProcess() {
|
||
var sampler = ProcessSampler()
|
||
let processes = sampler.sample(includeSystem: true)
|
||
#expect(processes.contains { $0.pid == getpid() && $0.isOwned && $0.threads != nil })
|
||
// 系统进程来自 ps:launchd(PID 1)属于 root
|
||
#expect(processes.contains { $0.pid == 1 && !$0.isOwned && $0.userName == "root" })
|
||
}
|
||
|
||
@Test func diskHasCapacity() throws {
|
||
let disk = try #require(DiskSampler.sample())
|
||
#expect(disk.total > 0)
|
||
#expect(disk.available <= disk.total)
|
||
}
|
||
}
|
||
|
||
@Suite struct NetworkParsingTests {
|
||
@Test func parsesNettopOutput() {
|
||
let output = """
|
||
,bytes_in,bytes_out,
|
||
launchd.1,0,0,
|
||
Google Chrome H.4521,120127,95191,
|
||
broken line
|
||
"""
|
||
let parsed = NetworkProcessSampler.parse(output)
|
||
#expect(parsed.count == 2)
|
||
#expect(parsed[4521]?.name == "Google Chrome H")
|
||
#expect(parsed[4521]?.download == 120_127)
|
||
#expect(parsed[4521]?.upload == 95_191)
|
||
}
|
||
|
||
@Test func parsesCloudflareTrace() {
|
||
let fields = PublicAddressLookup.parseTrace("fl=123\nip=203.0.113.24\nloc=CN\n")
|
||
#expect(fields["ip"] == "203.0.113.24")
|
||
#expect(fields["loc"] == "CN")
|
||
#expect(PublicAddressLookup.parseTrace("ip=<html>")["ip"] == nil)
|
||
}
|
||
|
||
@Test func matchesEchoReplyBySequenceAndToken() {
|
||
// 20 字节 IPv4 头 + ICMP 回显应答(标识符被内核改写为 0x8e84)
|
||
var reply = [UInt8](repeating: 0, count: 20)
|
||
reply[0] = 0x45
|
||
reply += [0, 0, 0, 0, 0x8E, 0x84, 0x00, 0x07, 0xDE, 0xAD, 0xBE, 0xEF]
|
||
#expect(ConnectivityProbe.matches(reply, count: reply.count, family: AF_INET, sequence: 7, token: 0xDEADBEEF))
|
||
#expect(!ConnectivityProbe.matches(reply, count: reply.count, family: AF_INET, sequence: 8, token: 0xDEADBEEF))
|
||
#expect(!ConnectivityProbe.matches(reply, count: reply.count, family: AF_INET, sequence: 7, token: 1))
|
||
}
|
||
|
||
@Test func computesInternetChecksum() {
|
||
let packet: [UInt8] = [8, 0, 0, 0, 0x12, 0x34, 0, 1]
|
||
let sum = ConnectivityProbe.checksum(packet)
|
||
var filled = packet
|
||
filled[2] = UInt8(sum >> 8)
|
||
filled[3] = UInt8(sum & 0xFF)
|
||
#expect(ConnectivityProbe.checksum(filled) == 0)
|
||
}
|
||
}
|
||
|
||
@Suite struct GeoParsingTests {
|
||
@Test func parsesCleanIPResponse() throws {
|
||
let json = #"{"ok":true,"ip":"32.5.140.2","hostname":"host.example.net","geo":{"country":"美国","country_code":"US","country_en":"United States","region":"佛罗里达州","city":"玛丽湖"},"geo_sources":[{"source":"maxmind","country_code":"US"},{"source":"ipinfo","region":"Florida","city":"Lake Mary"}],"network":{"asn":7018,"asn_name":"AT&T Enterprises, LLC","isp":"AT&T Enterprises, LLC","network_type":"Residential","connection_type":"Cable/DSL","native_or_broadcast":"NATIVE"},"risk":{"risk_score":2,"risk_label":"Very Clean","is_vpn":false,"is_proxy":false,"is_datacenter":false,"is_abuser":true},"purity":{"score":98,"grade":"A+","confidence":100,"ip_type":"Residential IP","is_native":true,"residential_probability":98,"recommendation":"该 IP 整体较干净"}}"#
|
||
let info = try #require(PublicAddressLookup.parseCleanIP(Data(json.utf8)))
|
||
#expect(info.countryCode == "US")
|
||
#expect(info.city == "玛丽湖" && info.cityEnglish == "Lake Mary" && info.regionEnglish == "Florida")
|
||
#expect(info.asn == "AS7018")
|
||
#expect(info.organization == "AT&T Enterprises, LLC")
|
||
#expect(info.hostname == "host.example.net")
|
||
#expect(info.networkType == "Residential" && info.connectionType == "Cable/DSL")
|
||
#expect(info.ipType == "Residential IP" && info.isNative == true && info.residentialProbability == 98)
|
||
#expect(info.purity == PublicAddresses.Purity(score: 98, grade: "A+", confidence: 100, recommendation: "该 IP 整体较干净"))
|
||
#expect(info.risk == PublicAddresses.Risk(score: 2, label: "Very Clean", flags: ["abuser"]))
|
||
#expect(info.reportURL?.absoluteString == "https://cleanip.io/32.5.140.2")
|
||
#expect(PublicAddressLookup.parseCleanIP(Data(#"{"ok":false,"error":"bad"}"#.utf8)) == nil)
|
||
}
|
||
|
||
@Test func rejectsUnsafeCountryCode() {
|
||
#expect(PublicAddressLookup.validCountryCode("cn") == "CN")
|
||
#expect(PublicAddressLookup.validCountryCode("../x") == nil)
|
||
#expect(PublicAddressLookup.validCountryCode("USA") == nil)
|
||
}
|
||
}
|
||
|
||
@Suite struct ProcessParsingTests {
|
||
@Test func parsesPSOutput() {
|
||
let output = """
|
||
1 0 99:12.26 28144 /sbin/launchd
|
||
412 88 712:02.63 378656 /System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer
|
||
913 501 0:00.51 9120 /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||
broken
|
||
"""
|
||
let entries = ProcessSampler.parsePS(output)
|
||
#expect(entries.count == 3)
|
||
#expect(entries[0].pid == 1 && entries[0].uid == 0)
|
||
#expect(entries[1].cpuTime == 712 * 60 + 2.63)
|
||
#expect(entries[1].residentBytes == 378_656 * 1024)
|
||
#expect(entries[2].command == "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
||
}
|
||
|
||
@Test func parsesCPUTimeWithHours() {
|
||
#expect(ProcessSampler.parseCPUTime("1:02:03.50") == 3723.5)
|
||
#expect(ProcessSampler.parseCPUTime("0:00.51") == 0.51)
|
||
#expect(ProcessSampler.parseCPUTime("abc") == nil)
|
||
}
|
||
|
||
@Test func readsSystemCounts() throws {
|
||
let counts = try #require(SystemCounts.read())
|
||
#expect(counts.processes > 0)
|
||
#expect(counts.threads >= counts.processes)
|
||
}
|
||
}
|