///|
pub(all) struct ModuleMeta {
name : String
version : String
readme : String
repository : String
license : String
} derive(Eq)
///|
pub(all) struct ImportInfo {
target : String
alias_name : String
owner : String
} derive(Eq)
///|
pub(all) struct SourceFile {
path : String
package_path : String
is_test : Bool
digest : String
mutation_points : Int
} derive(Eq)
///|
pub(all) struct TestFile {
path : String
package_path : String
kind : String
} derive(Eq)
///|
pub(all) struct PackageQuality {
path : String
source_count : Int
test_count : Int
public_api_count : Int
warnings : Array[String]
} derive(Eq)
///|
pub(all) struct MutationCandidate {
id : String
path : String
line : Int
kind : String
original : String
replacement : String
} derive(Eq)
///|
pub(all) struct FileCoverage {
path : String
covered : Int
total : Int
percentage : Int
} derive(Eq)
///|
pub(all) struct QualityReport {
name : String
version : String
readme : String
repository : String
license : String
packages : Array[PackageQuality]
imports : Array[ImportInfo]
source_files : Array[SourceFile]
test_files : Array[TestFile]
mutation_candidates : Array[MutationCandidate]
warnings : Array[String]
mutation_score : Int
mutants_killed : Int
mutants_tested : Int
survived_mutants : Array[MutationCandidate]
coverage_total_percentage : Int
coverage_files : Array[FileCoverage]
} derive(Eq)
///|
pub(all) struct GatePolicy {
min_project_tests : Int
require_package_tests : Bool
require_tests_for_mutants : Bool
require_readme : Bool
require_license : Bool
require_ci : Bool
min_mutation_score : Int
min_coverage : Int
} derive(Eq)
///|
pub(all) struct GateResult {
passed : Bool
failures : Array[String]
warnings : Array[String]
} derive(Eq)
///|
pub(all) enum SealError {
MissingProject(String)
MissingManifest(String)
FileReadFailed(String)
} derive(Eq)
///|
pub fn default_policy() -> GatePolicy {
{
min_project_tests: 2,
require_package_tests: true,
require_tests_for_mutants: true,
require_readme: true,
require_license: true,
require_ci: true,
min_mutation_score: 0,
min_coverage: 0,
}
}
///|
pub fn analyze_project(path : String) -> Result[QualityReport, SealError] {
if !fs_exists(path) {
return Err(MissingProject(path))
}
let manifest_path = join_path(path, "moon.mod")
if !fs_exists(manifest_path) {
return Err(MissingManifest(manifest_path))
}
let meta = parse_moon_mod(fs_read(manifest_path))
let files = fs_list_files(path)
let sources : Array[SourceFile] = []
let tests : Array[TestFile] = []
let imports : Array[ImportInfo] = []
for file in files {
if file.has_suffix("moon.pkg") {
let package_path = package_path_of(file)
for
item in parse_pkg_imports(package_path, fs_read(join_path(path, file))) {
imports.push(item)
}
} else if file.has_suffix(".mbt") {
let package_path = package_path_of(file)
if is_test_file(file) {
tests.push({
path: file,
package_path,
kind: if file.has_suffix("_wbtest.mbt") {
"whitebox"
} else {
"blackbox"
},
})
} else {
let content = fs_read(join_path(path, file))
sources.push({
path: file,
package_path,
is_test: false,
digest: stable_hash(content),
mutation_points: collect_mutants_for_file(file, content, 0).length(),
})
}
}
}
let mutants = collect_mutation_candidates(path, sources)
let packages = build_package_quality(path, files, sources, tests)
let warnings = collect_project_warnings(files, meta, packages)
let readme = if meta.readme.length() > 0 && array_has(files, meta.readme) {
meta.readme
} else {
""
}
let license = if meta.license.length() > 0 && array_has(files, "LICENSE") {
meta.license
} else {
""
}
Ok({
name: meta.name,
version: meta.version,
readme,
repository: meta.repository,
license,
packages,
imports,
source_files: sources,
test_files: tests,
mutation_candidates: mutants,
warnings,
mutation_score: 0,
mutants_killed: 0,
mutants_tested: 0,
survived_mutants: [],
coverage_total_percentage: 0,
coverage_files: [],
})
}
///|
pub fn parse_moon_mod(input : String) -> ModuleMeta {
let input = input.replace_all(old="\r\n", new="\n")
{
name: quoted_field(input, "name"),
version: quoted_field(input, "version"),
readme: quoted_field(input, "readme"),
repository: quoted_field(input, "repository"),
license: quoted_field(input, "license"),
}
}
///|
pub fn parse_pkg_imports(
package_path : String,
input : String,
) -> Array[ImportInfo] {
let input = input.replace_all(old="\r\n", new="\n")
let imports : Array[ImportInfo] = []
for raw_line in input.split("\n") {
let line = raw_line.trim().to_owned()
if line.length() > 0 && line.contains("\"") && line.contains("@") {
let target = between(line, "\"", "\"")
let alias_name = after(line, "@").trim(chars=", \t\r\n").to_owned()
if target.length() > 0 {
imports.push({ target, alias_name, owner: package_owner(target) })
}
}
}
ignore(package_path)
imports
}
///|
pub fn evaluate_gate(report : QualityReport, policy : GatePolicy) -> GateResult {
let failures : Array[String] = []
if report.test_files.length() < policy.min_project_tests {
failures.push(
"project has fewer than \{policy.min_project_tests} test files",
)
}
if policy.require_package_tests {
for pkg in report.packages {
if pkg.source_count > 0 && pkg.test_count == 0 {
failures.push("package " + pkg.path + " has source files but no tests")
}
}
}
if policy.require_tests_for_mutants &&
report.mutation_candidates.length() > 0 &&
report.test_files.length() == 0 {
failures.push("mutation candidates exist but no tests were found")
}
if policy.require_readme && !report_has_readme(report) {
failures.push("README.md is missing")
}
if policy.require_license && report.license.length() == 0 {
failures.push("LICENSE file is missing")
}
if policy.require_ci && has_warning(report, "missing CI workflow") {
failures.push("CI workflow is missing")
}
if policy.min_mutation_score > 0 &&
report.mutation_score < policy.min_mutation_score {
failures.push(
"mutation score " +
report.mutation_score.to_string() +
"% is below minimum " +
policy.min_mutation_score.to_string() +
"%",
)
}
if policy.min_coverage > 0 &&
report.coverage_total_percentage < policy.min_coverage {
failures.push(
"coverage " +
report.coverage_total_percentage.to_string() +
"% is below minimum " +
policy.min_coverage.to_string() +
"%",
)
}
{ passed: failures.length() == 0, failures, warnings: report.warnings }
}
///|
pub fn mutation_plan(report : QualityReport) -> Array[MutationCandidate] {
report.mutation_candidates
}
///|
pub fn render_report(report : QualityReport) -> String {
let out = StringBuilder()
out.write_string("MoonSeal Quality Report v1\n")
out.write_string("project: " + report.name + "\n")
out.write_string("version: " + report.version + "\n")
out.write_string("source-files: \{report.source_files.length()}\n")
out.write_string("test-files: \{report.test_files.length()}\n")
out.write_string(
"mutation-candidates: \{report.mutation_candidates.length()}\n",
)
if report.mutants_tested > 0 {
out.write_string(
"mutation-testing: tested=\{report.mutants_tested} killed=\{report.mutants_killed} score=\{report.mutation_score}%\n",
)
}
if report.coverage_total_percentage > 0 || report.coverage_files.length() > 0 {
out.write_string(
"code-coverage: total=\{report.coverage_total_percentage}%\n",
)
for f in report.coverage_files {
out.write_string(
" \{f.path}: \{f.percentage}% (\{f.covered}/\{f.total})\n",
)
}
}
for pkg in report.packages {
out.write_string(
"package: \{pkg.path} sources=\{pkg.source_count} tests=\{pkg.test_count} public-api=\{pkg.public_api_count}\n",
)
}
for warning in report.warnings {
out.write_string("warning: " + warning + "\n")
}
out.to_string()
}
///|
pub fn render_gate(result : GateResult) -> String {
let out = StringBuilder()
out.write_string(
if result.passed {
"MoonSeal gate: PASS\n"
} else {
"MoonSeal gate: FAIL\n"
},
)
for failure in result.failures {
out.write_string("failure: " + failure + "\n")
}
for warning in result.warnings {
out.write_string("warning: " + warning + "\n")
}
out.to_string()
}
///|
pub fn render_mutants(candidates : Array[MutationCandidate]) -> String {
let out = StringBuilder()
for candidate in candidates {
out.write_string(
"\{candidate.id} \{candidate.kind} \{candidate.path}:\{candidate.line} \{candidate.original} -> \{candidate.replacement}\n",
)
}
out.to_string()
}
///|
pub fn summarize(report : QualityReport) -> String {
"MoonSeal project=\{report.name} packages=\{report.packages.length()} sources=\{report.source_files.length()} tests=\{report.test_files.length()} mutants=\{report.mutation_candidates.length()} warnings=\{report.warnings.length()}"
}
///|
pub fn has_warning(report : QualityReport, warning : String) -> Bool {
if warning == "" {
return report.warnings.length() == 0
}
for item in report.warnings {
if item == warning {
return true
}
}
false
}
///|
pub fn has_failure(result : GateResult, failure : String) -> Bool {
for item in result.failures {
if item == failure {
return true
}
}
false
}
///|
pub fn format_error(err : SealError) -> String {
match err {
MissingProject(path) => "project path not found: " + path
MissingManifest(path) => "moon.mod not found: " + path
FileReadFailed(path) => "failed to read file: " + path
}
}
///|
fn build_package_quality(
root : String,
files : Array[String],
sources : Array[SourceFile],
tests : Array[TestFile],
) -> Array[PackageQuality] {
let packages : Array[PackageQuality] = []
for file in files {
if file.has_suffix("moon.pkg") {
let pkg = package_path_of(file)
let source_count = count_sources_in_package(sources, pkg)
let test_count = count_tests_in_package(tests, pkg)
let public_api_count = count_public_api(root, pkg)
let warnings : Array[String] = []
if source_count > 0 && test_count == 0 {
warnings.push("package " + pkg + " has source files but no tests")
}
packages.push({
path: pkg,
source_count,
test_count,
public_api_count,
warnings,
})
}
}
packages
}
///|
fn collect_project_warnings(
files : Array[String],
meta : ModuleMeta,
packages : Array[PackageQuality],
) -> Array[String] {
let warnings : Array[String] = []
if meta.readme.length() == 0 || !array_has(files, meta.readme) {
warnings.push("missing README.md")
}
if meta.license.length() == 0 || !array_has(files, "LICENSE") {
warnings.push("missing LICENSE")
}
if !has_ci_workflow(files) {
warnings.push("missing CI workflow")
}
if !meta.name.contains("/") {
warnings.push("nonstandard package name")
}
for pkg in packages {
for warning in pkg.warnings {
warnings.push(warning)
}
}
warnings
}
///|
fn collect_mutation_candidates(
root : String,
sources : Array[SourceFile],
) -> Array[MutationCandidate] {
let candidates : Array[MutationCandidate] = []
for source in sources {
let content = fs_read(join_path(root, source.path))
let found = collect_mutants_for_file(
source.path,
content,
candidates.length(),
)
for item in found {
candidates.push(item)
}
}
candidates
}
///|
fn collect_mutants_for_file(
path : String,
content : String,
offset : Int,
) -> Array[MutationCandidate] {
let content = content.replace_all(old="\r\n", new="\n")
let candidates : Array[MutationCandidate] = []
let mut line_no = 0
for view in content.split("\n") {
line_no += 1
let line = view.to_owned()
if line.contains("true") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"boolean-flip",
"true",
"false",
),
)
}
if line.contains("false") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"boolean-flip",
"false",
"true",
),
)
}
if line.contains(">=") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"comparison-boundary",
">=",
">",
),
)
} else if line.contains("<=") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"comparison-boundary",
"<=",
"<",
),
)
} else if line.contains(" > ") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"comparison-boundary",
">",
">=",
),
)
} else if line.contains(" < ") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"comparison-boundary",
"<",
"<=",
),
)
}
if line.contains("&&") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"logical-operator",
"&&",
"||",
),
)
}
if line.contains("||") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"logical-operator",
"||",
"&&",
),
)
}
if line.contains("= 0") || line.contains(" 0 ") || line.contains(" 0}") {
candidates.push(
make_candidate(
offset + candidates.length() + 1,
path,
line_no,
"integer-boundary",
"0",
"1",
),
)
}
}
candidates
}
///|
fn make_candidate(
n : Int,
path : String,
line : Int,
kind : String,
original : String,
replacement : String,
) -> MutationCandidate {
{ id: "MS-" + pad4(n), path, line, kind, original, replacement }
}
///|
fn count_sources_in_package(
sources : Array[SourceFile],
package_path : String,
) -> Int {
let mut count = 0
for source in sources {
if source.package_path == package_path {
count += 1
}
}
count
}
///|
fn count_tests_in_package(
test_files : Array[TestFile],
package_path : String,
) -> Int {
let mut count = 0
for item in test_files {
if item.package_path == package_path {
count += 1
}
}
count
}
///|
fn count_public_api(root : String, package_path : String) -> Int {
let interface_path = if package_path == "." {
join_path(root, "pkg.generated.mbti")
} else {
join_path(root, package_path + "/pkg.generated.mbti")
}
if !fs_exists(interface_path) {
return 0
}
let content = fs_read(interface_path).replace_all(old="\r\n", new="\n")
let mut count = 0
for raw_line in content.split("\n") {
let line = raw_line.trim().to_owned()
if line.has_prefix("pub fn ") ||
line.has_prefix("pub(all) struct ") ||
line.has_prefix("pub(all) enum ") {
count += 1
}
}
count
}
///|
fn report_has_readme(report : QualityReport) -> Bool {
report.readme.length() > 0
}
///|
fn is_test_file(path : String) -> Bool {
path.has_suffix("_test.mbt") || path.has_suffix("_wbtest.mbt")
}
///|
fn package_path_of(file : String) -> String {
if file == "moon.pkg" || !file.contains("/") {
"."
} else {
let mut last_slash = -1
for index in 0.. String {
let input = input.replace_all(old="\r\n", new="\n")
for raw_line in input.split("\n") {
let line = raw_line.trim().to_owned()
if line.has_prefix(key + " ") || line.has_prefix(key + "=") {
return between(line, "\"", "\"")
}
}
""
}
///|
fn between(input : String, left : String, right : String) -> String {
let start = input.find(left)
match start {
Some(a) => {
let rest = input[a + left.length():].to_owned()
let end = rest.find(right)
match end {
Some(b) => rest[:b].to_owned()
None => ""
}
}
None => ""
}
}
///|
fn after(input : String, marker : String) -> String {
let start = input.find(marker)
match start {
Some(a) => input[a + marker.length():].to_owned()
None => ""
}
}
///|
fn package_owner(target : String) -> String {
let slash = target.find("/")
match slash {
Some(index) => target[:index].to_owned()
None => target
}
}
///|
fn stable_hash(input : String) -> String {
let mut hash = 5381
for ch in input {
hash = (hash * 33 + ch.to_int()) % 1000003
}
hash.to_string()
}
///|
fn pad4(value : Int) -> String {
if value < 10 {
"000" + value.to_string()
} else if value < 100 {
"00" + value.to_string()
} else if value < 1000 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
fn array_has(items : Array[String], value : String) -> Bool {
for item in items {
if item == value {
return true
}
}
false
}
///|
fn has_ci_workflow(files : Array[String]) -> Bool {
for file in files {
if file.has_prefix(".github/workflows/") &&
(file.has_suffix(".yml") || file.has_suffix(".yaml")) {
return true
}
}
false
}
///|
fn join_path(root : String, child : String) -> String {
fs_join(root, child)
}
///|
extern "js" fn fs_exists(path : String) -> Bool =
#|(path) => require("fs").existsSync(path)
///|
extern "js" fn fs_read(path : String) -> String =
#|(path) => require("fs").readFileSync(path, "utf8")
///|
extern "js" fn fs_join(root : String, child : String) -> String =
#|(root, child) => require("path").join(root, child)
///|
extern "js" fn fs_list_files(root : String) -> Array[String] =
#|(root) => {
#| const fs = require("fs");
#| const path = require("path");
#| const out = [];
#| function walk(dir, prefix) {
#| for (const name of fs.readdirSync(dir).sort()) {
#| if (name === ".git" || name === "_build" || name === ".mooncakes" || name === ".repos" || name === "target") continue;
#| const full = path.join(dir, name);
#| const rel = prefix ? `${prefix}/${name}` : name;
#| const stat = fs.statSync(full);
#| if (stat.isDirectory()) walk(full, rel);
#| else out.push(rel);
#| }
#| }
#| walk(root, "");
#| return out;
#|}
///|
extern "js" fn fs_write(path : String, content : String) -> Unit =
#|(path, content) => require("fs").writeFileSync(path, content, "utf8")
///|
extern "js" fn exec_sync(cmd : String, cwd : String) -> Int =
#|(cmd, cwd) => {
#| try {
#| require("child_process").execSync(cmd, { cwd: cwd, stdio: "ignore" });
#| return 0;
#| } catch (e) {
#| return 1;
#| }
#|}
///|
extern "js" fn exec_output(cmd : String, cwd : String) -> String =
#|(cmd, cwd) => {
#| try {
#| return require("child_process").execSync(cmd, {
#| cwd: cwd,
#| encoding: "utf8",
#| stdio: ["ignore", "pipe", "ignore"],
#| });
#| } catch (e) {
#| return "";
#| }
#|}
///|
fn get_bool(map : Map[String, Json], key : String, default : Bool) -> Bool {
match map.get(key) {
Some(True) => true
Some(False) => false
_ => default
}
}
///|
fn get_int(map : Map[String, Json], key : String, default : Int) -> Int {
match map.get(key) {
Some(Number(v, ..)) => v.to_int()
_ => default
}
}
///|
pub fn load_policy(root : String) -> GatePolicy {
let default = default_policy()
let config_path = join_path(root, "moonseal.json")
if !fs_exists(config_path) {
return default
}
let content = fs_read(config_path)
try {
let json = @json.parse(content)
match json {
Object(m) => {
let min_project_tests = get_int(
m,
"min_project_tests",
default.min_project_tests,
)
let require_package_tests = get_bool(
m,
"require_package_tests",
default.require_package_tests,
)
let require_tests_for_mutants = get_bool(
m,
"require_tests_for_mutants",
default.require_tests_for_mutants,
)
let require_readme = get_bool(
m,
"require_readme",
default.require_readme,
)
let require_license = get_bool(
m,
"require_license",
default.require_license,
)
let require_ci = get_bool(m, "require_ci", default.require_ci)
let min_mutation_score = get_int(
m,
"min_mutation_score",
default.min_mutation_score,
)
let min_coverage = get_int(m, "min_coverage", default.min_coverage)
{
min_project_tests,
require_package_tests,
require_tests_for_mutants,
require_readme,
require_license,
require_ci,
min_mutation_score,
min_coverage,
}
}
_ => default
}
} catch {
_ => default
}
}
///|
fn mutate_line_content(
line : String,
original : String,
replacement : String,
) -> String {
let index = line.find(original)
match index {
Some(idx) => {
let before = line[:idx].to_owned()
let after = line[idx + original.length():].to_owned()
before + replacement + after
}
None => line
}
}
///|
fn mutate_file_content(
content : String,
line_no : Int,
original : String,
replacement : String,
) -> String {
let lines = content
.replace_all(old="\r\n", new="\n")
.split("\n")
.map(fn(s) { s.to_owned() })
.to_array()
if line_no > 0 && line_no <= lines.length() {
lines[line_no - 1] = mutate_line_content(
lines[line_no - 1],
original,
replacement,
)
}
let out = StringBuilder()
for i in 0.. (Int, Int, Int, Array[MutationCandidate]) {
let survived : Array[MutationCandidate] = []
let mut killed = 0
for candidate in candidates {
let file_path = join_path(root, candidate.path)
if !fs_exists(file_path) {
continue
}
let original_content = fs_read(file_path)
let mutated_content = mutate_file_content(
original_content,
candidate.line,
candidate.original,
candidate.replacement,
)
fs_write(file_path, mutated_content)
// Run the test suite using our wrapper to ensure it runs correctly on the CI
let exit_code = exec_sync("moon test --target js", root)
if exit_code != 0 {
killed += 1
} else {
survived.push(candidate)
}
fs_write(file_path, original_content)
}
let total = candidates.length()
let score = if total == 0 { 100 } else { killed * 100 / total }
(total, killed, score, survived)
}
///|
fn parse_int(s : String) -> Int {
let mut val = 0
for ch in s {
let code = ch.to_int()
if code >= 48 && code <= 57 {
val = val * 10 + (code - 48)
}
}
val
}
///|
pub fn run_coverage_analysis(root : String) -> (Int, Array[FileCoverage]) {
ignore(exec_sync("moon test --target js --enable-coverage", root))
let output = exec_output("moon coverage report -f summary", root)
let files : Array[FileCoverage] = []
let mut total_pct = 0
let lines = output
.replace_all(old="\r\n", new="\n")
.split("\n")
.map(fn(s) { s.to_owned() })
.to_array()
for line in lines {
let trimmed = line.trim().to_owned()
if trimmed.length() == 0 {
continue
}
let colon_idx = trimmed.find(":")
match colon_idx {
Some(idx) => {
let name = trimmed[:idx].to_owned().trim().to_owned()
let frac_str = trimmed[idx + 1:].to_owned().trim().to_owned()
let slash_idx = frac_str.find("/")
match slash_idx {
Some(s_idx) => {
let covered_str = frac_str[:s_idx].to_owned().trim().to_owned()
let total_str = frac_str[s_idx + 1:].to_owned().trim().to_owned()
let covered = parse_int(covered_str)
let total = parse_int(total_str)
let pct = if total == 0 { 100 } else { covered * 100 / total }
if name == "Total" {
total_pct = pct
} else {
files.push({ path: name, covered, total, percentage: pct })
}
}
None => ()
}
}
None => ()
}
}
(total_pct, files)
}
///|
pub impl ToJson for ImportInfo with fn to_json(self : ImportInfo) -> Json {
Json::object({
"target": Json::string(self.target),
"alias_name": Json::string(self.alias_name),
"owner": Json::string(self.owner),
})
}
///|
pub impl ToJson for SourceFile with fn to_json(self : SourceFile) -> Json {
Json::object({
"path": Json::string(self.path),
"package_path": Json::string(self.package_path),
"is_test": Json::boolean(self.is_test),
"digest": Json::string(self.digest),
"mutation_points": Json::number(self.mutation_points.to_double()),
})
}
///|
pub impl ToJson for TestFile with fn to_json(self : TestFile) -> Json {
Json::object({
"path": Json::string(self.path),
"package_path": Json::string(self.package_path),
"kind": Json::string(self.kind),
})
}
///|
pub impl ToJson for PackageQuality with fn to_json(self : PackageQuality) -> Json {
let warnings = Array::new()
for w in self.warnings {
warnings.push(Json::string(w))
}
Json::object({
"path": Json::string(self.path),
"source_count": Json::number(self.source_count.to_double()),
"test_count": Json::number(self.test_count.to_double()),
"public_api_count": Json::number(self.public_api_count.to_double()),
"warnings": Json::array(warnings),
})
}
///|
pub impl ToJson for MutationCandidate with fn to_json(self : MutationCandidate) -> Json {
Json::object({
"id": Json::string(self.id),
"path": Json::string(self.path),
"line": Json::number(self.line.to_double()),
"kind": Json::string(self.kind),
"original": Json::string(self.original),
"replacement": Json::string(self.replacement),
})
}
///|
pub impl ToJson for FileCoverage with fn to_json(self : FileCoverage) -> Json {
Json::object({
"path": Json::string(self.path),
"covered": Json::number(self.covered.to_double()),
"total": Json::number(self.total.to_double()),
"percentage": Json::number(self.percentage.to_double()),
})
}
///|
pub impl ToJson for QualityReport with fn to_json(self : QualityReport) -> Json {
let packages = Array::new()
for p in self.packages {
packages.push(p.to_json())
}
let imports = Array::new()
for i in self.imports {
imports.push(i.to_json())
}
let source_files = Array::new()
for sf in self.source_files {
source_files.push(sf.to_json())
}
let test_files = Array::new()
for tf in self.test_files {
test_files.push(tf.to_json())
}
let mutation_candidates = Array::new()
for mc in self.mutation_candidates {
mutation_candidates.push(mc.to_json())
}
let warnings = Array::new()
for w in self.warnings {
warnings.push(Json::string(w))
}
let survived_mutants = Array::new()
for sm in self.survived_mutants {
survived_mutants.push(sm.to_json())
}
let coverage_files = Array::new()
for cf in self.coverage_files {
coverage_files.push(cf.to_json())
}
Json::object({
"name": Json::string(self.name),
"version": Json::string(self.version),
"readme": Json::string(self.readme),
"repository": Json::string(self.repository),
"license": Json::string(self.license),
"packages": Json::array(packages),
"imports": Json::array(imports),
"source_files": Json::array(source_files),
"test_files": Json::array(test_files),
"mutation_candidates": Json::array(mutation_candidates),
"warnings": Json::array(warnings),
"mutation_score": Json::number(self.mutation_score.to_double()),
"mutants_killed": Json::number(self.mutants_killed.to_double()),
"mutants_tested": Json::number(self.mutants_tested.to_double()),
"survived_mutants": Json::array(survived_mutants),
"coverage_total_percentage": Json::number(
self.coverage_total_percentage.to_double(),
),
"coverage_files": Json::array(coverage_files),
})
}
///|
pub impl ToJson for GateResult with fn to_json(self : GateResult) -> Json {
let failures = Array::new()
for f in self.failures {
failures.push(Json::string(f))
}
let warnings = Array::new()
for w in self.warnings {
warnings.push(Json::string(w))
}
Json::object({
"passed": Json::boolean(self.passed),
"failures": Json::array(failures),
"warnings": Json::array(warnings),
})
}