File: StressTesterTool.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (284 lines) | stat: -rw-r--r-- 9,901 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import ArgumentParser
import Foundation
import Common
import SwiftSyntax
import Dispatch

public struct StressTesterTool: ParsableCommand {
  public static var configuration = CommandConfiguration(
    abstract: "A utility for finding sourcekitd crashes in a Swift source file"
  )

  @Option(name: [.long, .customShort("m")], help: """
    One of 'none' (default), 'basic', 'concurrent', 'insideOut', or 'typoed'
    """)
  public var rewriteMode: RewriteMode = .none

  @Option(name: .shortAndLong, help: """
    Format of results. Either 'json' or 'humanReadable'
    """)
  public var format: OutputFormat = .humanReadable

  @Option(name: .shortAndLong, help: ArgumentHelp("""
    The maximum number of AST builds (triggered by CodeComplete, \
    TypeContextInfo, ConformingMethodList and file modifications) to \
    allow per file
    """, valueName: "n"))
  public var limit: Int?

  @Flag(name: .long, help: """
    Print the stress tester actions as they are being executed to stdout.
    """)
  public var printActions: Bool = false

  @Flag(name: .long, help: """
    Print all request that are being sent to sourcekitd as JSON
    """)
  public var printRequests: Bool = false

  @Option(name: .shortAndLong, help: ArgumentHelp("""
    Divides the work for each file into <total> equal parts \
    and only performs the <page>th group. \
    --page is incompatible with --offset-filter.
    """, valueName: "page/total"))
  public var page: Page = Page()

  @Option(name: .long, help: ArgumentHelp("""
    If specified, only execute actions at this offset. \
    Useful to reproduce a specific failure locally. \
    --offset-filter is incompatible with --page.
    """))
  public var offsetFilter: Int?

  @Option(name: [.customLong("request"), .customShort("r")],
          help: "One of '\(RequestKind.allCases.map({ $0.rawValue }).joined(separator: "\", \""))'")
  public var requests: [RequestKind] = [.ide]

  @Flag(name: .shortAndLong, help: """
    Dump the sourcekitd requests the stress tester would perform instead of \
    performing them
    """)
  public var dryRun: Bool = false

  @Flag(name: .long, help: """
    Output sourcekitd's response to each request the stress tester makes
    """)
  public var reportResponses: Bool = false

  @Option(name: [.customLong("type-list-item"), .customShort("t")], help: """
    The USR of a conformed-to protocol to use for the ConformingMethodList \
    request
    """)
  public var conformingMethodsTypeList: [String] = ["s:SQ", "s:SH"] // Equatable and Hashable

  @Option(name: .long,
          help: "File to store aggregated measurements how long the SourceKit requests issued by the stress tester took",
          transform: URL.init(fileURLWithPath:))
  public var requestDurationsOutputFile: URL?

  @Option(name: .long,
          help: "Path to a temporary directory to store intermediate modules",
          transform: URL.init(fileURLWithPath:))
  public var tempDir: URL?

  @Option(name: .shortAndLong, help: """
    Path of swiftc to run, defaults to retrieving from xcrun if not given
    """)
  public var swiftc: String?

  @Option(help: """
  Extra code completion options to pass to sourcekitd for each code completion request in the 'key.codecomplete.options' dictionary.
  'key.codecomplete.' will automatically be prepended to these options.
  Key and value are separated by '='. E.g. --extra-code-complete-options hidelowpriority=1
  """)
  public var extraCodeCompleteOptions: [String] = []

  @Argument(help: "A Swift source file to stress test", completion: .file(),
            transform: URL.init(fileURLWithPath:))
  public var file: URL

  @Argument(help: "Swift compiler arguments for the provided file")
  public var compilerArgs: [CompilerArg]

  public init() {}

  private mutating func customValidate() throws {
    let hasFileCompilerArg = compilerArgs.contains { arg in
      arg.transformed.contains { $0 == file.path }
    }
    if !hasFileCompilerArg {
      throw ValidationError("\(file.path) missing from compiler args")
    }

    guard FileManager.default.fileExists(atPath: file.path) else {
      throw ValidationError("File does not exist at \(file.path)")
    }

    if swiftc == nil {
      swiftc = pathFromXcrun(for: "swiftc")
    }
    guard let swiftc = swiftc else {
      throw ValidationError("No swiftc given and no default could be determined")
    }
    guard FileManager.default.isExecutableFile(atPath: swiftc) else {
      throw ValidationError("swiftc at '\(swiftc)' is not executable")
    }

    if tempDir == nil {
      do {
        tempDir = try FileManager.default.url(
          for: .itemReplacementDirectory,
          in: .userDomainMask,
          appropriateFor: URL(fileURLWithPath: NSTemporaryDirectory()),
          create: true)
      } catch let error {
        throw ValidationError("Could not create temporary directory: \(error.localizedDescription)")
      }
    } else if !FileManager.default.fileExists(atPath: tempDir!.path) {
      throw ValidationError("Temporary directory \(tempDir!.path) does not exist")
    }

    if page != Page() && offsetFilter != nil {
      throw ValidationError("--page is incompatible with --offset-filter")
    }
  }

  public mutating func run() throws {
    // FIXME: Remove this and rename `customValidate` to `validate` once swift
    // is using an argument parser with c17e00a (ie. keeping mutations in
    // `validate`).
    try customValidate()

    let options = StressTesterOptions(
      requests: RequestKind.reduce(requests),
      rewriteMode: rewriteMode,
      conformingMethodsTypeList: conformingMethodsTypeList,
      page: page,
      offsetFilter: offsetFilter,
      tempDir: tempDir!,
      astBuildLimit: limit,
      printActions: printActions,
      printRequests: printRequests,
      requestDurationsOutputFile: requestDurationsOutputFile,
      responseHandler: !reportResponses ? nil :
        { [format] responseData in
          try StressTesterTool.report(
            StressTesterMessage.produced(responseData),
            as: format)
        },
      dryRun: !dryRun ? nil :
        { [format] actions in
          for action in actions {
            try StressTesterTool.report(action, as: format)
          }
        }
    )

    let processedArgs = CompilerArgs(for: file, args: compilerArgs, tempDir: tempDir!)
    let tester = StressTester(options: options)

    // Run the main stress tester loop on a background thread to leave the main
    // thread free for callbacks from SourceKit. Use a thread and not a dispatch
    // queue because dispatch queues have a reduced stack size that is
    // insufficient for SwiftSyntax parsing
    let thread = Thread { [self] in
      do {
        let extraCodeCompleteOptionsDict = try Dictionary<String, String>(extraCodeCompleteOptions.map({
          let split = $0.split(separator: "=")
          if split.count == 2 {
            return (String(split[0]), String(split[1]))
          } else {
            throw ValidationError("Invalid extra code completion option '\($0)'. Must be of the form <key>=<value>")
          }
        }), uniquingKeysWith: { old, new in new })
        let errors = tester.run(swiftc: swiftc!, compilerArgs: processedArgs, extraCodeCompleteOptions: extraCodeCompleteOptionsDict)

        if !errors.isEmpty {
          var hasOnlySoftErrors = true
          for error in errors {
            if let error = error as? SourceKitError {
              hasOnlySoftErrors = hasOnlySoftErrors && error.isSoft
              let message = StressTesterMessage.detected(error)
              try StressTesterTool.report(message, as: format)
            } else {
              throw error
            }
          }
          if !hasOnlySoftErrors {
            throw ExitCode.failure
          }
        }

        // Leave for debugging purposes if there was an error
        try FileManager.default.removeItem(at: tempDir!)
      } catch {
        StressTesterTool.exit(withError: error)
      }
      // The stress tester finished running. Exit the program.
      StressTesterTool.exit(withError: nil)
    }
    thread.stackSize = 8 << 20 // 8 MB.
    thread.start()
  }

  private static func report<T>(_ message: T, as format: OutputFormat) throws
  where T: Codable & CustomStringConvertible {
    switch format {
    case .humanReadable:
      print(String(describing: message))
    case .json:
      let data = try JSONEncoder().encode(message)
      print(String(data: data, encoding: .utf8)!)
    }
  }
}

public enum OutputFormat: String, ExpressibleByArgument {
  case humanReadable
  case json
}

extension Page: ExpressibleByArgument {
  public init?(argument: String) {
    let parts = argument.split(separator: "/", maxSplits: 1,
                               omittingEmptySubsequences: false)
    if parts.count == 2 {
      if let number = Int(parts[0]),
         let count = Int(parts[1]), number > 0,
         number <= count {
        self.init(number, of: count)
        return
      }
    }
    return nil
  }
}

extension RequestKind: ExpressibleByArgument {
  public init?(argument: String) {
    guard let kind = RequestKind.byName(argument) else {
      return nil
    }
    self = kind
  }
}

extension RewriteMode: ExpressibleByArgument {}

extension CompilerArg : ExpressibleByArgument {
  public init(argument: String) {
    self.init(argument)
  }
}