class SuggestionTestThread extends Thread {
    private final Random sampler = new Random(0);

    private final ConcurrentLinkedQueue<Pair<SuggestionTestResultData, String>> results;
    private JLanguageTool standardLt;
    private Rule standardRule;
    private final Map<SuggestionChangesExperiment, Rule> rules;
    private final BlockingQueue<SuggestionTestData> tasks;

    @SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
    SuggestionTestThread(BlockingQueue<SuggestionTestData> tasks, ConcurrentLinkedQueue<Pair<SuggestionTestResultData, String>> results) {
      rules = new HashMap<>();
      this.tasks = tasks;
      this.results = results;
    }

    @Override
    public void run() {
      Language lang = Languages.getLanguageForShortCode(SuggestionsChanges.getInstance().getConfig().language);
      init(lang);
      while (!isInterrupted()) {
        try {
          SuggestionTestData entry = tasks.poll(1L, TimeUnit.SECONDS);
          if (entry == null) {
            break;
          } else {
            doWork(entry);
          }
        } catch (InterruptedException | IOException e) {
          throw new RuntimeException(e);
        }
      }
    }


    private void init(Language lang) {
      Iterator<SuggestionChangesExperiment> iterator = SuggestionsChanges.getInstance().getExperiments().iterator();
      // parameters for experiments are shared via Singleton, so initialization must block
      synchronized (tasks) {
        SuggestionsChanges.getInstance().setCurrentExperiment(null);
        standardLt = new JLanguageTool(lang);
        standardRule = standardLt.getAllRules().stream().filter(Rule::isDictionaryBasedSpellingRule)
          .findFirst().orElse(null);

        while (iterator.hasNext()) {
          SuggestionChangesExperiment experiment = iterator.next();
          SuggestionsChanges.getInstance().setCurrentExperiment(experiment);

          JLanguageTool lt = new JLanguageTool(lang);
          try {
            lt.activateLanguageModelRules(new File(SuggestionsChanges.getInstance().getConfig().ngramLocation));
          } catch (IOException e) {
            throw new RuntimeException(e);
          }

          Rule spellerRule = lt.getAllRules().stream().filter(Rule::isDictionaryBasedSpellingRule)
            .findFirst().orElse(null);

          rules.put(experiment, spellerRule);
        }
      }
    }


    void doWork(SuggestionTestData entry) throws IOException, InterruptedException {<FILL_FUNCTION_BODY>}
  }


      AnalyzedSentence sentence = standardLt.getAnalyzedSentence(entry.getSentence());

      if (entry.getDataset().enforceCorrect) {
        AnalyzedSentence correction = standardLt.getAnalyzedSentence(entry.getCorrection());
        RuleMatch[] correctionMatches = standardRule.match(correction);
        if (correctionMatches.length != 0) {
          String message = String.format("Error found in sentence '%s', ignoring because of 'enforceCorrect' flag.%n", entry.getCorrection());
          results.add(Pair.of(new SuggestionTestResultData(entry, null), message));
          return;
        }
      }

      // needs to be here to make combined filtering + sampling more transparent
      if (sampler.nextFloat() > entry.getDataset().sampleRate) {
        return;
      }

      List<SuggestionChangesExperiment> experiments = SuggestionsChanges.getInstance().getExperiments();
      int experimentId = 0;

      StringBuilder message = new StringBuilder();
      message.append(String.format("Checking candidates for correction '%s' -> '%s' in sentence '%s':%n",
        entry.getCovered(), entry.getReplacement(), entry.getSentence()));
      List<String> correct = new ArrayList<>();
      List<RuleMatch> gatheredSuggestions = new ArrayList<>(experiments.size());
      int textSize = sentence.getText().length();
      for (SuggestionChangesExperiment experiment : experiments) {
        experimentId++;

        Rule spellerRule = rules.get(experiment);
        if (spellerRule == null) {
          continue;
        }
        long startTime = System.currentTimeMillis();
        RuleMatch[] matches = spellerRule.match(sentence);
        long computationTime = System.currentTimeMillis() - startTime;

        for (RuleMatch match : matches) {
          String matchedWord = sentence.getText().substring(match.getFromPos(), match.getToPos());
          if (!matchedWord.equals(entry.getCovered())) {
            //System.out.println("Other spelling error detected, ignoring: " + matchedWord + " / " + covered);
            continue;
          }
          List<String> suggestions = match.getSuggestedReplacements();
          gatheredSuggestions.add(match);
          if (suggestions.isEmpty()) { // TODO should be tracked as well
            continue;
          }
          int position = suggestions.indexOf(entry.getReplacement());
          SuggestionsChanges.getInstance().trackExperimentResult(Pair.of(experiment, entry.getDataset()),
            position, textSize, computationTime);
          if (position == 0) {
            correct.add(String.valueOf(experimentId));
          }

          message.append(String.format("Experiment #%d: %s -> accepted @ #%d%n", experimentId, suggestions, position));
        }
      }
      message.append(String.format("Correct suggestions by experiments: %s%n", String.join(", ", correct)));

      results.add(Pair.of(new SuggestionTestResultData(entry, gatheredSuggestions), message.toString()));
    
<no_super_class>