// --- sourcekit_fuzzer.swift - a simple code completion fuzzer --------------- // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2017 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 // // ---------------------------------------------------------------------------- // // The idea here is we start with a source file and proceed to place the cursor // at random locations in the file, eventually visiting all locations exactly // once in a shuffled random order. // // If completion at a location crashes, we run the test case through 'creduce' // to find a minimal reproducer that also crashes (possibly with a different // crash, but in practice all the examples I've seen continue to crash in the // same way as creduce performs its reduction). // // Once creduce fully reduces a test case, we save it to a file named // 'crash-NNN.swift', with a RUN: line suitable for placing the test case in // 'validation-tests/IDE/crashers_2'. // // The overall script execution stops once all source locations in the file // have been tested. // // You must first install creduce // somewhere in your $PATH. Then, run this script as follows: // // swift utils/sourcekit_fuzzer/sourcekit_fuzzer.swift // // - is your Swift build directory (the one with subdirectories // named swift-macosx-x86_64 and llvm-macosx-x86_64). // // - is the source file to fuzz. Try any complex but // self-contained Swift file that exercises a variety of language features; // for example, I've had good results with the files in test/Prototypes/. // // TODO: // - Add fuzzing for CursorInfo and RangeInfo // - Get it running on Linux // - Better error handling // - More user-friendly output import Darwin import Foundation // https://stackoverflow.com/questions/24026510/how-do-i-shuffle-an-array-in-swift/24029847 extension MutableCollection { /// Shuffles the contents of this collection. mutating func shuffle() { let c = count guard c > 1 else { return } for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) { let d: Int = numericCast(arc4random_uniform(numericCast(unshuffledCount))) guard d != 0 else { continue } let i = index(firstUnshuffled, offsetBy: d) swapAt(firstUnshuffled, i) } } } extension String { func write(to path: String) throws { try write(to: URL(fileURLWithPath: path), atomically: true, encoding: String.Encoding.utf8) } } // Gross enum ProcessError : Error { case failed } func run(_ args: [String]) throws -> Int32 { var pid: pid_t = 0 let argv = args.map { $0.withCString(strdup) } defer { argv.forEach { free($0) } } let envp = ProcessInfo.processInfo.environment.map { "\($0.0)=\($0.1)".withCString(strdup) } defer { envp.forEach { free($0) } } let result = posix_spawn(&pid, argv[0], nil, nil, argv + [nil], envp + [nil]) if result != 0 { throw ProcessError.failed } var stat: Int32 = 0 waitpid(pid, &stat, 0) return stat } var arguments = CommandLine.arguments // Workaround for behavior of CommandLine in script mode, where we don't drop // the filename argument from the list. if arguments.first == "sourcekit_fuzzer.swift" { arguments = Array(arguments[1...]) } if arguments.count != 2 { print("Usage: sourcekit_fuzzer ") exit(1) } let buildDir = arguments[0] let notPath = "\(buildDir)/llvm-macosx-x86_64/bin/not" let swiftIdeTestPath = "\(buildDir)/swift-macosx-x86_64/bin/swift-ide-test" let creducePath = "/usr/local/bin/creduce" let file = arguments[1] let contents = try! String(contentsOfFile: file) var offsets = Array(0...contents.count) offsets.shuffle() var good = 0 var bad = 0 for offset in offsets { print("TOTAL FAILURES: \(bad) out of \(bad + good)") let index = contents.index(contents.startIndex, offsetBy: offset) let prefix = contents[..