All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonarsource.ruby.plugin.SimpleCovSensor Maven / Gradle / Ivy

/*
 * SonarSource Ruby
 * Copyright (C) 2018-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonarsource.ruby.plugin;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.coverage.NewCoverage;
import org.sonar.api.config.Configuration;
import org.sonarsource.analyzer.commons.internal.json.simple.JSONArray;
import org.sonarsource.analyzer.commons.internal.json.simple.JSONObject;
import org.sonarsource.analyzer.commons.internal.json.simple.parser.JSONParser;

public class SimpleCovSensor implements Sensor {

  private static final Logger LOG = LoggerFactory.getLogger(SimpleCovSensor.class);
  private final RubyExclusionsFileFilter rubyExclusionsFileFilter;

  public SimpleCovSensor(RubyExclusionsFileFilter rubyExclusionsFileFilter) {
    this.rubyExclusionsFileFilter = rubyExclusionsFileFilter;
  }

  @Override
  public void describe(SensorDescriptor descriptor) {
    descriptor.name("SimpleCov Sensor for Ruby coverage")
      .onlyOnLanguage(RubyPlugin.RUBY_LANGUAGE_KEY);
  }

  @Override
  public void execute(SensorContext context) {
    try {
      Map reports = getReportFilesAndContents(context);
      if (reports.isEmpty()) {
        return;
      }
      
      JSONParser parser = new JSONParser();
      Map> mergedCoverages = new HashMap<>();
      reports.forEach((path, report) -> safeReadCoverageReport(parser, mergedCoverages, path, report));

      saveCoverage(context, mergedCoverages);
    } catch (IOException e) {
      LOG.error("Error reading coverage reports", e);
    }
  }

  private static void safeReadCoverageReport(JSONParser parser, Map> mergedCoverages, Path reportPath, String report) {
    try {
      JSONObject parseResult = (JSONObject) parser.parse(report);
      mergeFileCoverages(mergedCoverages, parseResult);
    } catch (Exception e) {
      LOG.error("Cannot read coverage report file, expecting standard SimpleCov JSON formatter output: '{}'", reportPath, e);
    }
  }

  private void saveCoverage(SensorContext context, Map> mergedCoverages) {
    FileSystem fileSystem = context.fileSystem();
    FilePredicates predicates = fileSystem.predicates();

    for (Entry> coverageForFile : mergedCoverages.entrySet()) {
      String filePath = coverageForFile.getKey();
      InputFile inputFile = fileSystem.inputFile(predicates.hasAbsolutePath(filePath));
      if (inputFile != null) {
        try {
          saveNewCoverage(context, coverageForFile.getValue(), inputFile);
        } catch (IllegalStateException e) {
          LOG.error("Invalid coverage information on file: '{}'", filePath, e);
        }
      } else if (rubyExclusionsFileFilter.isNotExcluded(filePath)){
        LOG.warn("File '{}' is present in coverage report but cannot be found in filesystem", filePath);
      }
    }
  }

  private static void saveNewCoverage(SensorContext context, Map hitsPerLines, InputFile inputFile) {
    NewCoverage newCoverage = context.newCoverage().onFile(inputFile);
    for (Entry hitsPerLine : hitsPerLines.entrySet()) {
      if (hitsPerLine.getValue() != null) {
        newCoverage.lineHits(hitsPerLine.getKey(), hitsPerLine.getValue());
      }
    }
    newCoverage.save();
  }

  private static void mergeFileCoverages(Map> coveragePerFiles, Map upperJsonObjects) {
    upperJsonObjects.forEach((key, value) -> {
      if ("coverage".equals(key)) {
        mergeFrameworkCoveragesFromJsonFormatter(coveragePerFiles, value);
      }
      JSONObject testFrameworkCoverage = (JSONObject) value.get("coverage");
      if (testFrameworkCoverage != null) {
        LOG.warn("Importing SimpleCov resultset JSON will not be supported from simplecov 18.0. Consider using the JSON formatter, available from SimpleCov 20.0");
        mergeFrameworkCoveragesFromResultSet(coveragePerFiles, testFrameworkCoverage);
      }
    });
  }

  private static void mergeFrameworkCoveragesFromJsonFormatter(Map> coveragePerFiles, Map fileCoverageObject) {
    fileCoverageObject.forEach((key, value) -> {
      JSONArray hitsPerLine = (JSONArray) value.get("lines");
      mergeHitPerLines(coveragePerFiles, key, hitsPerLine);
    });
  }

  private static void mergeFrameworkCoveragesFromResultSet(Map> coveragePerFiles, Map testFrameworkCoveragePerFiles) {
    testFrameworkCoveragePerFiles.forEach((key, value) -> mergeHitPerLines(coveragePerFiles, key, value));
  }

  private static void mergeHitPerLines(Map> coveragePerFiles, String currentFile, JSONArray hitsPerLine) {
    Map fileCoverage = coveragePerFiles.computeIfAbsent(currentFile, key -> new HashMap<>());
    for (int i = 0; i < hitsPerLine.size(); i++) {
      Object hits = hitsPerLine.get(i);
      int line = i + 1;
      Integer currentHits = fileCoverage.getOrDefault(line, 0);
      // Hits can be a Long (coverage data available), null or "ignored".
      if (hits instanceof Long) {
        fileCoverage.put(line, mergeHitsForLine(((Long) hits).intValue(), currentHits));
      } else if (hits == null) {
        fileCoverage.put(line, mergeHitsForLine(null, currentHits));
      }
    }
  }

  /*
   * A re-implementation of merging logic implemented by simplecov.
   * See https://github.com/simplecov-ruby/simplecov/blob/0e35b257e24381e4ec2c99b321954509ae21eaf0/lib/simplecov/combine/lines_combiner.rb#L20-L40
   *
   * VisibleForTesting
   */
  @CheckForNull
  static Integer mergeHitsForLine(@Nullable Integer first, @Nullable Integer second) {
    if (first == null) {
      if (second == null || second == 0) {
        return null;
      }
      return second;
    }
    if (second == null) {
      if (first == 0) {
        return null;
      }
      return first;
    }
    return first + second;
  }

  private static Map getReportFilesAndContents(SensorContext context) throws IOException {
    Map reports = new HashMap<>();
    Configuration config = context.config();
    FileSystem fs = context.fileSystem();
    for (String reportPath : config.getStringArray(RubyPlugin.REPORT_PATHS_KEY)) {
      String trimmedPath = reportPath.trim();
      String report = fileContent(fs, trimmedPath);
      if (report != null) {
        reports.put(Paths.get(trimmedPath), report);
      } else if (config.hasKey(RubyPlugin.REPORT_PATHS_KEY)) {
        LOG.error("SimpleCov report not found: '{}'", trimmedPath);
      }
    }
    return reports;
  }

  @CheckForNull
  private static String fileContent(FileSystem fs, String reportPath) throws IOException {
    InputFile report = fs.inputFile(fs.predicates().hasPath(reportPath));
    if (report != null && report.isFile()) {
      return report.contents();
    }
    File reportFile = fs.resolvePath(reportPath);
    if (reportFile.isFile()) {
      return new String(Files.readAllBytes(reportFile.toPath()), StandardCharsets.UTF_8);
    }
    return null;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy