// RealGIRParsingTests.swift // Cross-checks the parser against the system's real .gir files. Counts are // verified against independently-derived ground truth (grep over the same XML) // so that a parser regression that silently drops constructs is caught here // rather than surfacing as mysterious missing bindings downstream. import Foundation import Testing @testable import GObjectGeneratorCore @Suite("Real GIR parsing") struct RealGIRParsingTests { /// Directory holding the system's GObject Introspection files. nonisolated static let girDirectory = "/usr/share/gir-1.0" /// Whether a given GIR file is present on this system. /// /// Callable from `@Test` availability traits, which run outside the /// package's default MainActor isolation. /// /// - Parameter name: The GIR file name, e.g. `"Gtk-4.0.gir"`. /// - Returns: `true` when the file exists and tests depending on it can run. nonisolated static func hasGIR(_ name: String) -> Bool { FileManager.default.fileExists(atPath: "\(girDirectory)/\(name)") } /// Parses a system GIR file. /// /// - Parameter name: The GIR file name, e.g. `"Gtk-4.0.gir"`. /// - Returns: The parsed repository. /// - Throws: A `GIRParserError` if parsing fails. static func parse(_ name: String) throws -> Repository { try GIRParser().parse(fileURL: URL(fileURLWithPath: "\(girDirectory)/\(name)")) } /// Counts occurrences of a literal substring in a GIR file, as independent /// ground truth for what the parser should have found. /// /// Mirrors `grep -c` semantics for the single-occurrence-per-line markup /// these attributes appear in. /// /// - Parameters: /// - needle: The literal substring to count, e.g. `"direction=\"out\""`. /// - file: The GIR file name. /// - Returns: The number of lines containing the substring. static func groundTruthCount(of needle: String, in file: String) throws -> Int { let text = try String(contentsOfFile: "\(girDirectory)/\(file)", encoding: .utf8) return text.split(separator: "\n").count { $0.contains(needle) } } @Test(.enabled(if: hasGIR("GLib-2.0.gir"))) func parsesGLib() throws { let repo = try Self.parse("GLib-2.0.gir") let ns = try #require(repo.namespaces.first { $0.name == "GLib" }) #expect(ns.version == "2.0") #expect(ns.records.count > 50) #expect(ns.functions.count > 100) #expect(ns.enumerations.count > 10) // Every record field must have a real type. `.void` fields were the // signature of the old parser's dropped routing. let voidFields = ns.records.flatMap { record in record.fields.filter { $0.type == .void }.map { "\(record.name).\($0.name)" } } #expect(voidFields.isEmpty, "record fields with no type: \(voidFields.prefix(5))") } @Test(.enabled(if: hasGIR("Gtk-4.0.gir"))) func parsesGtkMatchingGroundTruth() throws { let repo = try Self.parse("Gtk-4.0.gir") let ns = try #require(repo.namespaces.first { $0.name == "Gtk" }) #expect(ns.classes.count == (try Self.groundTruthCount(of: " ns.records.count / 2) } @Test(.enabled(if: hasGIR("Gtk-4.0.gir"))) func capturesOutParametersAndThrows() throws { let repo = try Self.parse("Gtk-4.0.gir") let ns = try #require(repo.namespaces.first { $0.name == "Gtk" }) /// Every callable reachable from the namespace, reduced to the two /// facts under test: its parameters and whether it throws. var parameterLists: [[Parameter]] = [] var throwsFlags: [Bool] = [] func record(_ parameters: [Parameter], _ throwsGError: Bool) { parameterLists.append(parameters) throwsFlags.append(throwsGError) } for cls in ns.classes { for m in cls.methods { record(m.parameters, m.throwsGError) } for c in cls.constructors { record(c.parameters, c.throwsGError) } for f in cls.functions { record(f.parameters, f.throwsGError) } } for iface in ns.interfaces { for m in iface.methods { record(m.parameters, m.throwsGError) } } for rec in ns.records { for m in rec.methods { record(m.parameters, m.throwsGError) } } for f in ns.functions { record(f.parameters, f.throwsGError) } let outParams = parameterLists.flatMap { $0 }.count { $0.direction == .out } #expect(outParams > 300, "expected GTK's out-parameters to be parsed, found \(outParams)") let throwing = throwsFlags.count { $0 } #expect(throwing > 30, "expected GTK's throwing callables to be parsed, found \(throwing)") // gtk_widget_measure is the canonical multi-out-parameter method. let widget = try #require(ns.classes.first { $0.name == "Widget" }) let measure = try #require(widget.methods.first { $0.name == "measure" }) #expect(measure.parameters.count { $0.direction == .out } == 4) } @Test(.enabled(if: hasGIR("Gtk-4.0.gir"))) func capturesClassHierarchyAndGTypes() throws { let repo = try Self.parse("Gtk-4.0.gir") let ns = try #require(repo.namespaces.first { $0.name == "Gtk" }) let widget = try #require(ns.classes.first { $0.name == "Widget" }) #expect(widget.parent == "GObject.InitiallyUnowned") #expect(widget.isAbstract) #expect(widget.getTypeFunction == "gtk_widget_get_type") #expect(widget.implements.contains("Accessible")) // Every class must carry its GType registration: the registry needs it // to classify types and drive GValue access. let missingGType = ns.classes.filter { $0.getTypeFunction == nil }.map(\.name) #expect(missingGType.isEmpty, "classes missing glib:get-type: \(missingGType.prefix(5))") } /// The registry must link the real GTK stack across module boundaries. /// Under the old generator this chain was severed at every namespace /// boundary, so `Gtk.Widget` did not inherit from `GObject.Object` at all. @Test( .enabled( if: hasGIR("Gtk-4.0.gir") && hasGIR("Gio-2.0.gir") && hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir"))) func registryLinksRealGtkStackAcrossModules() throws { // Mirrors the tier configs: Gtk's stack is only coherent with Gio // present, since Gtk.Application inherits Gio.Application. let registry = TypeRegistry(repositories: [ "GLib": try Self.parse("GLib-2.0.gir"), "GObject": try Self.parse("GObject-2.0.gir"), "Gio": try Self.parse("Gio-2.0.gir"), "Gtk": try Self.parse("Gtk-4.0.gir"), ]) let ancestry = registry.ancestry(of: "Gtk.Button").map(\.girName) #expect(ancestry.contains("Gtk.Widget")) #expect(ancestry.contains("GObject.InitiallyUnowned")) #expect(ancestry.last == "GObject.Object") // Floating-reference rule: widgets sink, plain GObjects do not. let buttonFloats = registry.descendsFromInitiallyUnowned("Gtk.Button") let appFloats = registry.descendsFromInitiallyUnowned("Gtk.Application") #expect(buttonFloats) #expect(!appFloats) #expect(registry.isGObject("Gtk.Application")) #expect(registry.ancestry(of: "Gtk.Application").map(\.girName).contains("Gio.Application")) // Cross-module `open` requirement: GObject.Object is subclassed only // from other modules, and must still be open. let subclassed = registry.subclassedTypes() #expect(subclassed.contains("GObject.Object")) #expect(subclassed.contains("GObject.InitiallyUnowned")) // Gtk includes Gdk and Gsk; with neither loaded, both must resolve as // foreign so callables touching them skip cleanly rather than emitting // fabricated type references. let foreign = registry.foreign #expect(foreign.contains("Gdk")) #expect(foreign.contains("Gsk")) let surface = registry.resolve(.typeRef("Surface", namespace: "Gdk"), from: "Gtk") #expect(surface?.category == .foreign(namespace: "Gdk")) } /// cairo ships no GIR on this system, but Gdk references it. Loading Gdk /// must therefore surface cairo as foreign rather than as an unknown type. @Test(.enabled(if: hasGIR("Gdk-4.0.gir") && hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir"))) func cairoResolvesAsForeignWhenGdkIsLoaded() throws { let registry = TypeRegistry(repositories: [ "GLib": try Self.parse("GLib-2.0.gir"), "GObject": try Self.parse("GObject-2.0.gir"), "Gdk": try Self.parse("Gdk-4.0.gir"), ]) let foreign = registry.foreign #expect(foreign.contains("cairo")) let context = registry.resolve(.typeRef("Context", namespace: "cairo"), from: "Gdk") #expect(context?.category == .foreign(namespace: "cairo")) } /// Every class parent named by the real GIRs must resolve. An unresolvable /// parent means a fabricated superclass reference in generated code. @Test( .enabled( if: hasGIR("Gtk-4.0.gir") && hasGIR("Gio-2.0.gir") && hasGIR("GObject-2.0.gir") && hasGIR("GLib-2.0.gir"))) func everyRealGtkClassParentResolves() throws { let repos: [String: Repository] = [ "GLib": try Self.parse("GLib-2.0.gir"), "GObject": try Self.parse("GObject-2.0.gir"), "Gio": try Self.parse("Gio-2.0.gir"), "Gtk": try Self.parse("Gtk-4.0.gir"), ] let registry = TypeRegistry(repositories: repos) let ns = try #require(repos["Gtk"]?.namespaces.first { $0.name == "Gtk" }) let unresolved = ns.classes.compactMap { cls -> String? in guard let parent = cls.parent else { return nil } let qualified = parent.contains(".") ? parent : "Gtk.\(parent)" return registry.resolve(girName: qualified) == nil ? "\(cls.name) -> \(parent)" : nil } #expect(unresolved.isEmpty, "unresolvable class parents: \(unresolved.prefix(5))") } @Test(.enabled(if: hasGIR("Gtk-4.0.gir"))) func capturesNonIntrospectableSymbols() throws { let repo = try Self.parse("Gtk-4.0.gir") let ns = try #require(repo.namespaces.first { $0.name == "Gtk" }) let nonIntrospectable = ns.classes.flatMap { cls in cls.methods.filter { !$0.symbolInfo.isIntrospectable } } #expect(!nonIntrospectable.isEmpty, "GTK has non-introspectable methods that must be recognized") } /// Every filename rendered from the real tier-1 GIRs must follow the /// PascalCase convention — the end-to-end lock that keeps future codegen /// from reintroducing symbol-named files (`boxedFree.swift`, /// `PARAM_MASK.swift`, `cclosureMarshalBOOLEANFLAGS.swift`). @Test(.enabled(if: hasGIR("GLib-2.0.gir") && hasGIR("GObject-2.0.gir"))) func generatedFilenamesArePascalCase() throws { let repositories = [ "GLib": try Self.parse("GLib-2.0.gir"), "GObject": try Self.parse("GObject-2.0.gir"), ] let analysis = MultiPackageAnalysis( repositories: repositories, directDependencies: [:], transitiveDependencies: [:], implicitImports: [:], packageConfigs: [:] ) let registry = TypeRegistry(repositories: repositories) let plans = planModules(analysis: analysis, registry: registry) #expect(plans.count == 2) for (module, plan) in plans { let files = renderModule(plan) let violations = files.keys.filter { !isValidGeneratedFileName($0) } #expect(violations.isEmpty, "\(module) emitted non-PascalCase filenames: \(violations.sorted())") // The merge targets must actually exist — an empty module would // vacuously pass the check above. #expect(files["Functions.swift"] != nil) #expect(files["Constants.swift"] != nil) } } }