Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-resilience

Resilience pipelines for Pudu: retry, circuit breaker, timeout, fallback, hedging, rate limiting, and chaos injection

0.1.0Apache-2.01

InstallClose

Mutate.pudu

Pudu209 lines7.7 KB

GitHub ↗
1/** @Tools.Mutate.Harness — mutation testing over the source tree */2module Mutate34import Std.Char as Char5import Std.Env as Env6import Std.Io as Io7import Std.List as List8import Std.Option as Option9import Std.Process as Process10import Std.Result as Result11import Std.Text as Text1213/** @Tools.Mutate.Mutant — one single-point change to one file */14type Mutant = { file: Str, line: Int, column: Int, from: Str, to: Str }1516/** @Tools.Mutate.Verdict — what the checks said about one mutant */17type Verdict = Killed | Survived | Invalid1819/// Operator replacements, tried at every code position in this order.20const OPERATORS: Array[(Str, Str)] = [21  ("==", "!="), ("!=", "=="), ("<=", "<"), (">=", ">"), ("<", "<="), (">", ">="),22  ("&&", "||"), ("||", "&&"), ("true", "false"), ("false", "true"), ("+ 1", "- 1"), ("- 1", "+ 1"), ("!", "")23]2425/// Longest a mutant's test run may take before it counts as killed.26const MUTANT_TIMEOUT_MS: Int = 600002728/// The pure layer, mutated by `--domain`.29const DOMAIN: Str = "src/PuduLangResilience/Domain"3031/// Runs the harness and answers 0, or 1 when the score is below the threshold.32fn main() -> Int {33  let every = Option.unwrapOr(Option.andThen(Env.option("--every"), |given: Str| Text.countOf(given)), 1)34  let threshold = Option.unwrapOr(Option.andThen(Env.option("--threshold"), |given: Str| Text.countOf(given)), 0)35  let files = match Env.option("--file") {36    case Some(one) => [one]37    case None => sources(if Env.hasFlag("--domain") { DOMAIN } else { "src" })38  }39  var all: Array[Mutant] = []40  for file in files {41    all = all.concat(mutantsOf(file, Result.unwrapOr(Io.read(file), "")))42  }43  var chosen: Array[Mutant] = []44  var index = 045  for mutant in all {46    if index % (if every > 0 { every } else { 1 }) == 0 { chosen = chosen.push(mutant) }47    index = index + 148  }49  if Env.hasFlag("--dry-run") {50    for mutant in chosen { let _listed = Io.writeLine(describe(&mutant)) }51    let _counted = Io.writeLine(show(chosen.length()) + " mutants")52    return 053  }54  let compiler = Env.variableOr("PUDU_BIN", "pudu")55  if !Process.isAvailable(compiler) {56    let _said = Io.writeErrorLine("Mutate: " + compiler + " was not found; set PUDU_BIN")57    return 158  }59  var killed = 060  var survived = 061  var invalid = 062  var number = 063  for mutant in chosen {64    number = number + 165    match judge(compiler, &mutant) {66      case Killed => { killed = killed + 1 }67      case Survived => {68        survived = survived + 169        let _reported = Io.writeLine("survived  " + describe(&mutant))70      }71      case Invalid => { invalid = invalid + 1 }72    }73    let _progress = Io.writeErrorLine("[" + show(number) + "/" + show(chosen.length()) + "] " + describe(&mutant))74  }75  let scored = killed + survived76  let score = if scored == 0 { 100 } else { killed * 100 / scored }77  let _summary = Io.writeLine("killed " + show(killed) + ", survived " + show(survived) + ", invalid " + show(invalid) + ", score " + show(score) + "%")78  if score < threshold { 1 } else { 0 }79}8081/// Applies one mutant, runs the checks, restores the file, and answers the verdict.82fn judge(compiler: Str, mutant: &Mutant) -> Verdict {83  let original = Result.unwrapOr(Io.read(mutant.file), "")84  let _mutated = Io.write(mutant.file, applied(original, mutant))85  let verdict = match Process.within(compiler, &["check", mutant.file], MUTANT_TIMEOUT_MS) {86    case Ok(Some(checked)) => {87      if checked.status != 0 || checked.output.contains("error[") {88        Invalid89      } else {90        match Process.within(compiler, &["test", "test"], MUTANT_TIMEOUT_MS) {91          case Ok(Some(tested)) => if tested.status == 0 { Survived } else { Killed }92          case Ok(None) => Killed93          case Err(_) => Invalid94        }95      }96    }97    case _ => Invalid98  }99  let _restored = Io.write(mutant.file, original)100  verdict101}102103/// Every mutant of one file's text.104fn mutantsOf(file: Str, text: Str) -> Array[Mutant] {105  var found: Array[Mutant] = []106  var lineNumber = 0107  for line in text.split("\n") {108    lineNumber = lineNumber + 1109    let trimmed = line.trim()110    if !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("import ") {111      let code = codeMask(line)112      for operator in OPERATORS {113        var at = 0114        while at < line.length() {115          if matchesAt(line, &code, at, operator[0]) {116            found = found.push(Mutant{file: file, line: lineNumber, column: at + 1, from: operator[0], to: operator[1]})117          }118          at = at + 1119        }120      }121    }122  }123  found124}125126/// Whether an operator occurs at a code position and is not part of a longer operator or word.127fn matchesAt(line: Str, code: &Array[Bool], at: Int, operator: Str) -> Bool {128  let end = at + operator.length()129  if end > line.length() || line.slice(at, end) != operator { return false }130  var position = at131  while position < end {132    if !code[position] { return false }133    position = position + 1134  }135  let before = if at > 0 { line.slice(at - 1, at) } else { "" }136  let after = if end < line.length() { line.slice(end, end + 1) } else { "" }137  if operator == "true" || operator == "false" { return !isWord(before) && !isWord(after) }138  if operator == "<" { return after != "=" && after != "-" && before != "<" && isSpaced(before, after) }139  if operator == ">" { return after != "=" && before != "-" && before != "=" && before != ">" && isSpaced(before, after) }140  if operator == "!" { return after != "=" && (isWord(after) || after == "(") }141  if operator == "- 1" || operator == "+ 1" { return !isWord(after) }142  true143}144145/// Whether a comparison has a space on both sides, which a type's brackets never do.146fn isSpaced(before: Str, after: Str) -> Bool { before == " " && after == " " }147148/// For each character of a line, whether it is code rather than a string literal or comment.149fn codeMask(line: Str) -> Array[Bool] {150  var mask: Array[Bool] = []151  var inString = false152  var escaped = false153  var commented = false154  var index = 0155  let characters = line.chars()156  for character in characters {157    if commented {158      mask = mask.push(false)159    } else if inString {160      mask = mask.push(false)161      if escaped { escaped = false } else if character == '\\' { escaped = true } else if character == '"' { inString = false }162    } else if character == '"' {163      inString = true164      mask = mask.push(false)165    } else if character == '/' && index + 1 < characters.length() && characters[index + 1] == '/' {166      commented = true167      mask = mask.push(false)168    } else if character == '\'' {169      mask = mask.push(false)170    } else {171      mask = mask.push(true)172    }173    index = index + 1174  }175  mask176}177178/// The text with one mutant applied.179fn applied(text: Str, mutant: &Mutant) -> Str {180  let lines = text.split("\n")181  let target = lines[mutant.line - 1]182  let start = mutant.column - 1183  let changed = target.slice(0, start) + mutant.to + target.drop(start + mutant.from.length())184  lines.slice(0, mutant.line - 1).concat([changed]).concat(lines.slice(mutant.line, lines.length())).join("\n")185}186187/// A mutant as `file:line:column  from -> to`.188fn describe(mutant: &Mutant) -> Str {189  mutant.file + ":" + show(mutant.line) + ":" + show(mutant.column) + "  " + mutant.from + " -> " + (if mutant.to.isEmpty() { "(removed)" } else { mutant.to })190}191192/// Whether text is one identifier character.193fn isWord(text: Str) -> Bool {194  !text.isEmpty() && Text.allChars(text, |c: Char| Char.isAlphanumeric(c) || c == '_')195}196197/// Every source file under a directory, sorted.198fn sources(directory: Str) -> Array[Str] {199  var found: Array[Str] = []200  for path in List.sortBy(&Result.unwrapOr(Io.listPaths(directory), []), fn(left: Str, right: Str) -> Bool { left < right }) {201    if path.endsWith(".pudu") {202      found = found.push(path)203    } else if !path.contains(".") {204      found = found.concat(sources(path))205    }206  }207  found208}209