/* * Copyright 2023, gRPC Authors All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project // // Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import XCTest @testable import GRPCCodeGen final class Test_TextBasedRenderer: XCTestCase { func testComment() throws { try _test( .inline( #""" Generated by foo Also, bar """# ), renderedBy: TextBasedRenderer.renderComment, rendersAs: #""" // Generated by foo // // Also, bar """# ) try _test( .doc( #""" Generated by foo Also, bar """# ), renderedBy: TextBasedRenderer.renderComment, rendersAs: #""" /// Generated by foo /// /// Also, bar """# ) try _test( .mark("Lorem ipsum", sectionBreak: false), renderedBy: TextBasedRenderer.renderComment, rendersAs: #""" // MARK: Lorem ipsum """# ) try _test( .mark("Lorem ipsum", sectionBreak: true), renderedBy: TextBasedRenderer.renderComment, rendersAs: #""" // MARK: - Lorem ipsum """# ) try _test( .inline( """ Generated by foo\r\nAlso, bar """ ), renderedBy: TextBasedRenderer.renderComment, rendersAs: #""" // Generated by foo // Also, bar """# ) try _test( .preFormatted("/// Lorem ipsum\n"), renderedBy: TextBasedRenderer.renderComment, rendersAs: """ /// Lorem ipsum """ ) try _test( .preFormatted("/// Lorem ipsum\n\n/// Lorem ipsum\n"), renderedBy: TextBasedRenderer.renderComment, rendersAs: """ /// Lorem ipsum /// Lorem ipsum """ ) } func testImports() throws { try _test(nil, renderedBy: TextBasedRenderer.renderImports, rendersAs: "") try _test( [ImportDescription(moduleName: "Foo"), ImportDescription(moduleName: "Bar")], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" import Foo import Bar """# ) try _test( [ImportDescription(moduleName: "Foo", spi: "Secret")], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" @_spi(Secret) import Foo """# ) try _test( [ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"]))], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" #if os(Bar) || os(Baz) @preconcurrency import Foo #else import Foo #endif """# ) try _test( [ ImportDescription(moduleName: "Foo", preconcurrency: .always), ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always), ], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" @preconcurrency import Foo @preconcurrency @_spi(Secret) import Bar """# ) try _test( [ ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .typealias, name: "Bar") ), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .struct, name: "Baz") ), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .class, name: "Bac") ), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .enum, name: "Bap") ), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .protocol, name: "Bat") ), ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .let, name: "Bam")), ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .var, name: "Bag")), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .func, name: "Bak") ), ImportDescription( moduleName: "Foo", spi: "Secret", item: ImportDescription.Item(kind: .func, name: "SecretBar") ), ImportDescription( moduleName: "Foo", preconcurrency: .always, item: ImportDescription.Item(kind: .func, name: "PreconcurrencyBar") ), ], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" import typealias Foo.Bar import struct Foo.Baz import class Foo.Bac import enum Foo.Bap import protocol Foo.Bat import let Foo.Bam import var Foo.Bag import func Foo.Bak @_spi(Secret) import func Foo.SecretBar @preconcurrency import func Foo.PreconcurrencyBar """# ) } func testAccessModifiers() throws { try _test( .public, renderedBy: TextBasedRenderer.renderedAccessModifier, rendersAs: #""" public """# ) try _test( .internal, renderedBy: TextBasedRenderer.renderedAccessModifier, rendersAs: #""" internal """# ) try _test( .fileprivate, renderedBy: TextBasedRenderer.renderedAccessModifier, rendersAs: #""" fileprivate """# ) try _test( .private, renderedBy: TextBasedRenderer.renderedAccessModifier, rendersAs: #""" private """# ) } func testLiterals() throws { try _test( .string("hi"), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" "hi" """# ) try _test( .string("this string: \"foo\""), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" #"this string: "foo""# """# ) try _test( .nil, renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" nil """# ) try _test( .array([]), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" [] """# ) try _test( .array([.literal(.nil)]), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" [ nil ] """# ) try _test( .array([.literal(.nil), .literal(.nil)]), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" [ nil, nil ] """# ) } func testExpression() throws { try _test( .literal(.nil), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" nil """# ) try _test( .identifierPattern("foo"), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" foo """# ) try _test( .memberAccess(.init(left: .identifierPattern("foo"), right: "bar")), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" foo.bar """# ) try _test( .functionCall( .init( calledExpression: .identifierPattern("callee"), arguments: [.init(label: nil, expression: .identifierPattern("foo"))] ) ), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" callee(foo) """# ) } func testDeclaration() throws { try _test( .variable(kind: .let, left: "foo"), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" let foo """# ) try _test( .extension(.init(onType: "String", declarations: [])), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" extension String { } """# ) try _test( .struct(.init(name: "Foo")), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" struct Foo {} """# ) try _test( .protocol(.init(name: "Foo")), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" protocol Foo {} """# ) try _test( .enum(.init(name: "Foo")), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" enum Foo {} """# ) try _test( .typealias(.init(name: "foo", existingType: .member(["Foo", "Bar"]))), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" typealias foo = Foo.Bar """# ) try _test( .function(FunctionDescription.init(kind: .function(name: "foo"), body: [])), renderedBy: TextBasedRenderer.renderDeclaration, rendersAs: #""" func foo() {} """# ) } func testFunctionKind() throws { try _test( .initializer, renderedBy: TextBasedRenderer.renderedFunctionKind, rendersAs: #""" init """# ) try _test( .function(name: "funky"), renderedBy: TextBasedRenderer.renderedFunctionKind, rendersAs: #""" func funky """# ) try _test( .function(name: "funky", isStatic: true), renderedBy: TextBasedRenderer.renderedFunctionKind, rendersAs: #""" static func funky """# ) } func testFunctionKeyword() throws { try _test( .throws, renderedBy: TextBasedRenderer.renderedFunctionKeyword, rendersAs: #""" throws """# ) try _test( .async, renderedBy: TextBasedRenderer.renderedFunctionKeyword, rendersAs: #""" async """# ) } func testParameter() throws { try _test( .init(label: "l", name: "n", type: .member("T"), defaultValue: .literal(.nil)), renderedBy: TextBasedRenderer.renderParameter, rendersAs: #""" l n: T = nil """# ) try _test( .init(label: nil, name: "n", type: .member("T"), defaultValue: .literal(.nil)), renderedBy: TextBasedRenderer.renderParameter, rendersAs: #""" _ n: T = nil """# ) try _test( .init(label: "l", name: nil, type: .member("T"), defaultValue: .literal(.nil)), renderedBy: TextBasedRenderer.renderParameter, rendersAs: #""" l: T = nil """# ) try _test( .init(label: nil, name: nil, type: .member("T"), defaultValue: .literal(.nil)), renderedBy: TextBasedRenderer.renderParameter, rendersAs: #""" _: T = nil """# ) try _test( .init(label: nil, name: nil, type: .member("T"), defaultValue: nil), renderedBy: TextBasedRenderer.renderParameter, rendersAs: #""" _: T """# ) } func testGenericFunction() throws { try _test( .init( accessModifier: .public, kind: .function(name: "f"), generics: [.member("R")], parameters: [], whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]), body: [] ), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" public func f() where R: Sendable {} """# ) try _test( .init( accessModifier: .public, kind: .function(name: "f"), generics: [.member("R"), .member("T")], parameters: [], whereClause: WhereClause(requirements: [ .conformance("R", "Sendable"), .conformance("T", "Encodable"), ]), body: [] ), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" public func f() where R: Sendable, T: Encodable {} """# ) } func testFunction() throws { try _test( .init(accessModifier: .public, kind: .function(name: "f"), parameters: [], body: []), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" public func f() {} """# ) try _test( .init( accessModifier: .public, kind: .function(name: "f"), parameters: [.init(label: "a", name: "b", type: .member("C"), defaultValue: nil)], body: [] ), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" public func f(a b: C) {} """# ) try _test( .init( accessModifier: .public, kind: .function(name: "f"), parameters: [ .init(label: "a", name: "b", type: .member("C"), defaultValue: nil), .init(label: nil, name: "d", type: .member("E"), defaultValue: .literal(.string("f"))), ], body: [] ), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" public func f( a b: C, _ d: E = "f" ) {} """# ) try _test( .init( kind: .function(name: "f"), parameters: [], keywords: [.async, .throws], returnType: .identifierType(TypeName.string) ), renderedBy: TextBasedRenderer.renderFunction, rendersAs: #""" func f() async throws -> Swift.String """# ) } func testIdentifiers() throws { try _test( .pattern("foo"), renderedBy: TextBasedRenderer.renderIdentifier, rendersAs: #""" foo """# ) } func testMemberAccess() throws { try _test( .init(left: .identifierPattern("foo"), right: "bar"), renderedBy: TextBasedRenderer.renderMemberAccess, rendersAs: #""" foo.bar """# ) try _test( .init(left: nil, right: "bar"), renderedBy: TextBasedRenderer.renderMemberAccess, rendersAs: #""" .bar """# ) } func testFunctionCallArgument() throws { try _test( .init(label: "foo", expression: .identifierPattern("bar")), renderedBy: TextBasedRenderer.renderFunctionCallArgument, rendersAs: #""" foo: bar """# ) try _test( .init(label: nil, expression: .identifierPattern("bar")), renderedBy: TextBasedRenderer.renderFunctionCallArgument, rendersAs: #""" bar """# ) } func testFunctionCall() throws { try _test( .functionCall(.init(calledExpression: .identifierPattern("callee"))), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" callee() """# ) try _test( .functionCall( .init( calledExpression: .identifierPattern("callee"), arguments: [.init(label: "foo", expression: .identifierPattern("bar"))] ) ), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" callee(foo: bar) """# ) try _test( .functionCall( .init( calledExpression: .identifierPattern("callee"), arguments: [ .init(label: "foo", expression: .identifierPattern("bar")), .init(label: "baz", expression: .identifierPattern("boo")), ] ) ), renderedBy: TextBasedRenderer.renderExpression, rendersAs: #""" callee( foo: bar, baz: boo ) """# ) } func testExtension() throws { try _test( .init( accessModifier: .public, onType: "Info", declarations: [.variable(kind: .let, left: "foo", type: .member("Int"))] ), renderedBy: TextBasedRenderer.renderExtension, rendersAs: #""" public extension Info { let foo: Int } """# ) } func testDeprecation() throws { try _test( .init(), renderedBy: TextBasedRenderer.renderDeprecation, rendersAs: #""" @available(*, deprecated) """# ) try _test( .init(message: "some message"), renderedBy: TextBasedRenderer.renderDeprecation, rendersAs: #""" @available(*, deprecated, message: "some message") """# ) try _test( .init(renamed: "newSymbol(param:)"), renderedBy: TextBasedRenderer.renderDeprecation, rendersAs: #""" @available(*, deprecated, renamed: "newSymbol(param:)") """# ) try _test( .init(message: "some message", renamed: "newSymbol(param:)"), renderedBy: TextBasedRenderer.renderDeprecation, rendersAs: #""" @available(*, deprecated, message: "some message", renamed: "newSymbol(param:)") """# ) } func testAvailability() throws { try _test( .init(osVersions: [ .init(os: .macOS, version: "12.0"), .init(os: .iOS, version: "13.1.2"), .init(os: .watchOS, version: "8.1.2"), .init(os: .tvOS, version: "15.0.2"), ]), renderedBy: TextBasedRenderer.renderAvailability, rendersAs: #""" @available(macOS 12.0, iOS 13.1.2, watchOS 8.1.2, tvOS 15.0.2, *) """# ) } func testBindingKind() throws { try _test( .var, renderedBy: TextBasedRenderer.renderedBindingKind, rendersAs: #""" var """# ) try _test( .let, renderedBy: TextBasedRenderer.renderedBindingKind, rendersAs: #""" let """# ) } func testVariable() throws { try _test( .init( accessModifier: .public, isStatic: true, kind: .let, left: .identifierPattern("foo"), type: .init(TypeName.string), right: .literal(.string("bar")) ), renderedBy: TextBasedRenderer.renderVariable, rendersAs: #""" public static let foo: Swift.String = "bar" """# ) try _test( .init( accessModifier: .internal, isStatic: false, kind: .var, left: .identifierPattern("foo"), type: nil, right: nil ), renderedBy: TextBasedRenderer.renderVariable, rendersAs: #""" internal var foo """# ) try _test( .init( kind: .var, left: .identifierPattern("foo"), type: .init(TypeName.int), getter: [CodeBlock.expression(.literal(.int(42)))] ), renderedBy: TextBasedRenderer.renderVariable, rendersAs: #""" var foo: Swift.Int { 42 } """# ) try _test( .init( kind: .var, left: .identifierPattern("foo"), type: .init(TypeName.int), getter: [CodeBlock.expression(.literal(.int(42)))], getterEffects: [.throws] ), renderedBy: TextBasedRenderer.renderVariable, rendersAs: #""" var foo: Swift.Int { get throws { 42 } } """# ) } func testStruct() throws { try _test( .init(name: "Structy"), renderedBy: TextBasedRenderer.renderStruct, rendersAs: #""" struct Structy {} """# ) } func testProtocol() throws { try _test( .init(name: "Protocoly"), renderedBy: TextBasedRenderer.renderProtocol, rendersAs: #""" protocol Protocoly {} """# ) } func testEnum() throws { try _test( .init(name: "Enumy"), renderedBy: TextBasedRenderer.renderEnum, rendersAs: #""" enum Enumy {} """# ) } func testCodeBlockItem() throws { try _test( .declaration(.variable(kind: .let, left: "foo")), renderedBy: TextBasedRenderer.renderCodeBlockItem, rendersAs: #""" let foo """# ) try _test( .expression(.literal(.nil)), renderedBy: TextBasedRenderer.renderCodeBlockItem, rendersAs: #""" nil """# ) } func testCodeBlock() throws { try _test( .init( comment: .inline("- MARK: Section"), item: .declaration(.variable(kind: .let, left: "foo")) ), renderedBy: TextBasedRenderer.renderCodeBlock, rendersAs: #""" // - MARK: Section let foo """# ) try _test( .init(comment: nil, item: .declaration(.variable(kind: .let, left: "foo"))), renderedBy: TextBasedRenderer.renderCodeBlock, rendersAs: #""" let foo """# ) } func testTypealias() throws { try _test( .init(name: "inty", existingType: .member("Int")), renderedBy: TextBasedRenderer.renderTypealias, rendersAs: #""" typealias inty = Int """# ) try _test( .init(accessModifier: .private, name: "inty", existingType: .member("Int")), renderedBy: TextBasedRenderer.renderTypealias, rendersAs: #""" private typealias inty = Int """# ) } func testFile() throws { try _test( .init( topComment: .inline("hi"), imports: [.init(moduleName: "Foo")], codeBlocks: [.init(comment: nil, item: .declaration(.struct(.init(name: "Bar"))))] ), renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi import Foo struct Bar {} """# ) } func testIndentation() throws { try _test( .init( topComment: .inline("hi"), imports: [.init(moduleName: "Foo")], codeBlocks: [ .init( comment: nil, item: .declaration(.struct(.init(name: "Bar", members: [.struct(.init(name: "Baz"))]))) ) ] ), renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi import Foo struct Bar { struct Baz {} } """#, indentation: 2 ) try _test( .array([.literal(.nil), .literal(.nil)]), renderedBy: TextBasedRenderer.renderLiteral, rendersAs: #""" [ nil, nil ] """#, indentation: 3 ) try _test( .init( kind: .var, left: .identifierPattern("foo"), type: .init(TypeName.int), getter: [CodeBlock.expression(.literal(.int(42)))], getterEffects: [.throws] ), renderedBy: TextBasedRenderer.renderVariable, rendersAs: #""" var foo: Swift.Int { get throws { 42 } } """#, indentation: 5 ) } } extension Test_TextBasedRenderer { func _test( _ input: Input, renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> String), rendersAs output: String, file: StaticString = #file, line: UInt = #line, indentation: Int = 4 ) throws { let renderer = TextBasedRenderer(indentation: indentation) XCTAssertEqual(renderClosure(renderer)(input), output, file: file, line: line) } func _test( _ input: Input, renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> Void), rendersAs output: String, file: StaticString = #file, line: UInt = #line, indentation: Int = 4 ) throws { try _test( input, renderedBy: { renderer in let closure = renderClosure(renderer) return { input in closure(input) return renderer.renderedContents() } }, rendersAs: output, indentation: indentation ) } }