001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.batch.sensor.internal;
021
022import com.google.common.annotations.Beta;
023import com.google.common.collect.HashBasedTable;
024import com.google.common.collect.Table;
025import java.io.File;
026import java.io.Serializable;
027import java.nio.file.Path;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.EnumMap;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import javax.annotation.CheckForNull;
036import org.sonar.api.SonarQubeVersion;
037import org.sonar.api.batch.AnalysisMode;
038import org.sonar.api.batch.fs.InputModule;
039import org.sonar.api.batch.fs.internal.DefaultFileSystem;
040import org.sonar.api.batch.fs.internal.DefaultInputModule;
041import org.sonar.api.batch.fs.internal.DefaultTextPointer;
042import org.sonar.api.batch.rule.ActiveRules;
043import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
044import org.sonar.api.batch.sensor.Sensor;
045import org.sonar.api.batch.sensor.SensorContext;
046import org.sonar.api.batch.sensor.coverage.CoverageType;
047import org.sonar.api.batch.sensor.coverage.NewCoverage;
048import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage;
049import org.sonar.api.batch.sensor.cpd.NewCpdTokens;
050import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens;
051import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
052import org.sonar.api.batch.sensor.highlighting.TypeOfText;
053import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting;
054import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule;
055import org.sonar.api.batch.sensor.issue.Issue;
056import org.sonar.api.batch.sensor.issue.NewIssue;
057import org.sonar.api.batch.sensor.issue.internal.DefaultIssue;
058import org.sonar.api.batch.sensor.measure.Measure;
059import org.sonar.api.batch.sensor.measure.NewMeasure;
060import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
061import org.sonar.api.config.Settings;
062import org.sonar.api.internal.SonarQubeVersionFactory;
063import org.sonar.api.measures.Metric;
064import org.sonar.api.utils.System2;
065import org.sonar.api.utils.Version;
066import org.sonar.duplications.internal.pmd.TokensLine;
067
068/**
069 * Utility class to help testing {@link Sensor}.
070 * 
071 * Usage: call {@link #create(File)} to create an "in memory" implementation of {@link SensorContext} then
072 * pass it to your {@link Sensor}. You can then query elements provided by your sensor using methods {@link #allIssues()}, ...
073 * 
074 * @since 5.1
075 */
076@Beta
077public class SensorContextTester implements SensorContext {
078
079  private Settings settings;
080  private DefaultFileSystem fs;
081  private ActiveRules activeRules;
082  private InMemorySensorStorage sensorStorage;
083  private InputModule module;
084  private SonarQubeVersion sqVersion;
085
086  private SensorContextTester(Path moduleBaseDir) {
087    this.settings = new Settings();
088    this.fs = new DefaultFileSystem(moduleBaseDir);
089    this.activeRules = new ActiveRulesBuilder().build();
090    this.sensorStorage = new InMemorySensorStorage();
091    this.module = new DefaultInputModule("projectKey");
092    this.sqVersion = SonarQubeVersionFactory.create(System2.INSTANCE);
093  }
094
095  public static SensorContextTester create(File moduleBaseDir) {
096    return new SensorContextTester(moduleBaseDir.toPath());
097  }
098
099  public static SensorContextTester create(Path moduleBaseDir) {
100    return new SensorContextTester(moduleBaseDir);
101  }
102
103  @Override
104  public Settings settings() {
105    return settings;
106  }
107
108  public SensorContextTester setSettings(Settings settings) {
109    this.settings = settings;
110    return this;
111  }
112
113  @Override
114  public DefaultFileSystem fileSystem() {
115    return fs;
116  }
117
118  public SensorContextTester setFileSystem(DefaultFileSystem fs) {
119    this.fs = fs;
120    return this;
121  }
122
123  @Override
124  public ActiveRules activeRules() {
125    return activeRules;
126  }
127
128  public SensorContextTester setActiveRules(ActiveRules activeRules) {
129    this.activeRules = activeRules;
130    return this;
131  }
132
133  /**
134   * Default value is the version of this API. You can override it
135   * using {@link #setSonarQubeVersion(Version)} to test your Sensor behavior.
136   * @since 5.5
137   */
138  @Override
139  public Version getSonarQubeVersion() {
140    return sqVersion.get();
141  }
142
143  /**
144   * @since 5.5
145   */
146  public SensorContextTester setSonarQubeVersion(Version version) {
147    this.sqVersion = new SonarQubeVersion(version);
148    return this;
149  }
150
151  @Override
152  public InputModule module() {
153    return module;
154  }
155
156  @Override
157  public <G extends Serializable> NewMeasure<G> newMeasure() {
158    return new DefaultMeasure<>(sensorStorage);
159  }
160
161  public Collection<Measure> measures(String componentKey) {
162    return sensorStorage.measuresByComponentAndMetric.row(componentKey).values();
163  }
164
165  public <G extends Serializable> Measure<G> measure(String componetKey, Metric<G> metric) {
166    return measure(componetKey, metric.key());
167  }
168
169  public <G extends Serializable> Measure<G> measure(String componentKey, String metricKey) {
170    return sensorStorage.measuresByComponentAndMetric.row(componentKey).get(metricKey);
171  }
172
173  @Override
174  public NewIssue newIssue() {
175    return new DefaultIssue(sensorStorage);
176  }
177
178  public Collection<Issue> allIssues() {
179    return sensorStorage.allIssues;
180  }
181
182  @CheckForNull
183  public Integer lineHits(String fileKey, CoverageType type, int line) {
184    Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey);
185    if (defaultCoverageByType == null) {
186      return null;
187    }
188    if (defaultCoverageByType.containsKey(type)) {
189      return defaultCoverageByType.get(type).hitsByLine().get(line);
190    }
191    return null;
192  }
193
194  @CheckForNull
195  public Integer conditions(String fileKey, CoverageType type, int line) {
196    Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey);
197    if (defaultCoverageByType == null) {
198      return null;
199    }
200    if (defaultCoverageByType.containsKey(type)) {
201      return defaultCoverageByType.get(type).conditionsByLine().get(line);
202    }
203    return null;
204  }
205
206  @CheckForNull
207  public Integer coveredConditions(String fileKey, CoverageType type, int line) {
208    Map<CoverageType, DefaultCoverage> defaultCoverageByType = sensorStorage.coverageByComponent.get(fileKey);
209    if (defaultCoverageByType == null) {
210      return null;
211    }
212    if (defaultCoverageByType.containsKey(type)) {
213      return defaultCoverageByType.get(type).coveredConditionsByLine().get(line);
214    }
215    return null;
216  }
217
218  @CheckForNull
219  public List<TokensLine> cpdTokens(String componentKey) {
220    DefaultCpdTokens defaultCpdTokens = sensorStorage.cpdTokensByComponent.get(componentKey);
221    return defaultCpdTokens != null ? defaultCpdTokens.getTokenLines() : null;
222  }
223
224  @Override
225  public NewHighlighting newHighlighting() {
226    return new DefaultHighlighting(sensorStorage);
227  }
228
229  @Override
230  public NewCoverage newCoverage() {
231    return new DefaultCoverage(sensorStorage);
232  }
233
234  @Override
235  public NewCpdTokens newCpdTokens() {
236    return new DefaultCpdTokens(settings, sensorStorage);
237  }
238
239  public List<TypeOfText> highlightingTypeAt(String componentKey, int line, int lineOffset) {
240    DefaultHighlighting syntaxHighlightingData = sensorStorage.highlightingByComponent.get(componentKey);
241    if (syntaxHighlightingData == null) {
242      return Collections.emptyList();
243    }
244    List<TypeOfText> result = new ArrayList<>();
245    DefaultTextPointer location = new DefaultTextPointer(line, lineOffset);
246    for (SyntaxHighlightingRule sortedRule : syntaxHighlightingData.getSyntaxHighlightingRuleSet()) {
247      if (sortedRule.range().start().compareTo(location) <= 0 && sortedRule.range().end().compareTo(location) > 0) {
248        result.add(sortedRule.getTextType());
249      }
250    }
251    return result;
252  }
253
254  public static class MockAnalysisMode implements AnalysisMode {
255    private boolean isPreview = false;
256    private boolean isIssues = false;
257
258    @Override
259    public boolean isPreview() {
260      return isPreview;
261    }
262
263    public void setPreview(boolean value) {
264      this.isPreview = value;
265    }
266
267    @Override
268    public boolean isIssues() {
269      return this.isIssues;
270    }
271
272    public void setIssues(boolean issues) {
273      this.isIssues = issues;
274    }
275
276    @Override
277    public boolean isPublish() {
278      return !isPreview && !isIssues;
279    }
280  }
281
282  private static class InMemorySensorStorage implements SensorStorage {
283
284    private Table<String, String, Measure> measuresByComponentAndMetric = HashBasedTable.create();
285
286    private Collection<Issue> allIssues = new ArrayList<>();
287
288    private Map<String, DefaultHighlighting> highlightingByComponent = new HashMap<>();
289    private Map<String, DefaultCpdTokens> cpdTokensByComponent = new HashMap<>();
290    private Map<String, Map<CoverageType, DefaultCoverage>> coverageByComponent = new HashMap<>();
291
292    @Override
293    public void store(Measure measure) {
294      measuresByComponentAndMetric.row(measure.inputComponent().key()).put(measure.metric().key(), measure);
295    }
296
297    @Override
298    public void store(Issue issue) {
299      allIssues.add(issue);
300    }
301
302    @Override
303    public void store(DefaultHighlighting highlighting) {
304      highlightingByComponent.put(highlighting.inputFile().key(), highlighting);
305    }
306
307    @Override
308    public void store(DefaultCoverage defaultCoverage) {
309      String key = defaultCoverage.inputFile().key();
310      if (!coverageByComponent.containsKey(key)) {
311        coverageByComponent.put(key, new EnumMap<CoverageType, DefaultCoverage>(CoverageType.class));
312      }
313      coverageByComponent.get(key).put(defaultCoverage.type(), defaultCoverage);
314    }
315
316    @Override
317    public void store(DefaultCpdTokens defaultCpdTokens) {
318      cpdTokensByComponent.put(defaultCpdTokens.inputFile().key(), defaultCpdTokens);
319    }
320
321  }
322
323}