Files

96 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Render native icons into memory and inspect pixels; no app, windows or screenshots."""
from pathlib import Path
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory(prefix="lemana-icons-") as temporary:
folder = Path(temporary)
source = folder / "IconChecks.swift"
source.write_text(r'''
import AppKit
func pixels(size: Int = 96, _ draw: (CGContext) -> Void) -> [UInt8] {
let context = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8, bytesPerRow: size * 4,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)!
context.translateBy(x: 0, y: CGFloat(size))
context.scaleBy(x: CGFloat(size) / 24, y: -CGFloat(size) / 24)
draw(context)
return Array(UnsafeBufferPointer(start: context.data!.assumingMemoryBound(to: UInt8.self), count: size * size * 4))
}
func session(_ style: VPNIndicatorStyle, _ progress: Double?, dark: Bool, urgency: SessionTiming.Urgency = .normal) -> [UInt8] {
pixels { VPNIcons.drawSession(style: style, progress: progress, urgency: urgency, in: $0, dark: dark) }
}
func pixel(_ bytes: [UInt8], _ x: Int, _ y: Int) -> [UInt8] {
let index = (y * 4 * 96 + x * 4) * 4
return Array(bytes[index..<index + 4])
}
@main struct IconChecks {
static func main() {
precondition(VPNIndicatorStyle(rawValue: "ring") == .ring)
precondition(VPNIndicatorStyle(rawValue: "clock") == .clock)
precondition(VPNIndicatorStyle(rawValue: "time") == .time)
for dark in [false, true] {
let image = VPNIcons.sessionImage(style: .shield, progress: 0.5, urgency: .normal, dark: dark)
precondition(image.size == NSSize(width: 16, height: 16))
let native = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!
let actual = pixels { context in
context.translateBy(x: 0, y: 24)
context.scaleBy(x: 1, y: -1)
context.draw(native, in: CGRect(x: 0, y: 0, width: 24, height: 24))
}
precondition(pixel(actual, 12, 8)[3] < pixel(actual, 12, 18)[3], "native NSImage must fill from bottom")
var seen = Set<[UInt8]>()
for icon in VPNIcon.allCases {
let bytes = pixels { VPNIcons.draw(icon, in: $0, dark: dark) }
precondition(bytes.contains(where: { $0 != 0 }), "empty icon: \(icon)")
precondition(seen.insert(bytes).inserted, "duplicate icon: \(icon)")
for size in [16, 32] {
let compact = pixels(size: size) { VPNIcons.draw(icon, in: $0, dark: dark) }
let count = stride(from: 3, to: compact.count, by: 4).filter { compact[$0] > 30 }.count
precondition(count > 10, "invisible icon at \(size): \(icon)")
}
}
for style in [VPNIndicatorStyle.shield, .ring] {
let full = session(style, 0, dark: dark)
let quarter = session(style, 0.75, dark: dark)
let half = session(style, 0.5, dark: dark)
let spent = session(style, 1, dark: dark)
precondition(pixel(full, 12, 8)[3] > 200, "session must start brightly filled")
precondition((75...80).contains(pixel(spent, 12, 18)[3]), "spent interior must stay visible")
precondition(pixel(half, 7, 10) == pixel(spent, 7, 10), "spent part must dim from top")
precondition(pixel(half, 12, 18) == pixel(full, 12, 18), "remaining part must stay bright")
precondition(pixel(quarter, 12, 18) == pixel(half, 12, 18))
precondition(pixel(quarter, 7, 14) == pixel(spent, 7, 14))
precondition(pixel(full, 12, 18) == pixel(half, 12, 18))
precondition(pixel(full, 1, 1)[3] == 0, "fill escaped its shape")
precondition(session(style, -1, dark: dark) == full)
precondition(session(style, 2, dark: dark) == spent)
precondition(session(style, nil, dark: dark) == pixels { VPNIcons.draw(.noTime, in: $0, dark: dark) })
precondition(session(style, .nan, dark: dark) == session(style, nil, dark: dark))
let warning = pixel(session(style, 0.5, dark: dark, urgency: .warning), 12, 18)
let critical = pixel(session(style, 0.5, dark: dark, urgency: .critical), 12, 18)
precondition(warning != pixel(half, 12, 18) && warning != critical, "urgency colors must differ")
// Expiry stays legible over the dimmed interior in either theme.
let expired = session(style, 1, dark: dark, urgency: .critical)
precondition(pixel(expired, 12, 8)[3] > pixel(expired, 12, 18)[3])
}
precondition(session(.clock, 0.25, dark: dark) != session(.clock, 0.75, dark: dark))
precondition(session(.time, 0.5, dark: dark) != session(.time, 1, dark: dark))
}
print("PASS native icons: both themes, distinct states/actions, 16/32 px visibility, bright remaining fill, dim spent area, clipping, urgency, expiry and unknown time")
}
}
''')
binary = folder / "icon-check"
subprocess.run(["swiftc", "-parse-as-library", "-module-cache-path", str(ROOT / "app/.build/log-test-module-cache"),
str(ROOT / "app/Sources/LemanaVPN/SessionTiming.swift"),
str(ROOT / "app/Sources/LemanaVPN/VPNIcons.swift"), str(source), "-o", str(binary)], check=True)
subprocess.run([str(binary)], check=True)