ReadPLColorsJob.java

1
package edu.ucsb.cs.scaffold.jobs;
2
3
import edu.ucsb.cs.scaffold.entity.PatCredential;
4
import edu.ucsb.cs.scaffold.entity.PlColor;
5
import edu.ucsb.cs.scaffold.enums.PatPlatform;
6
import edu.ucsb.cs.scaffold.repository.PatCredentialRepository;
7
import edu.ucsb.cs.scaffold.repository.PlColorRepository;
8
import edu.ucsb.cs.scaffold.services.GithubService;
9
import edu.ucsb.cs.scaffold.services.PatEncryptionService;
10
import edu.ucsb.cs156.jobs.services.JobContext;
11
import edu.ucsb.cs156.jobs.services.JobContextConsumer;
12
import java.util.Optional;
13
import java.util.regex.Matcher;
14
import java.util.regex.Pattern;
15
import lombok.Builder;
16
import org.springframework.web.client.HttpClientErrorException;
17
18
/**
19
 * Reads PrairieLearn's badge color map from GitHub (issue #96) and upserts the {@code pl_color}
20
 * table with any colors that are new or whose hex code has changed. Launched by an admin (POST
21
 * /api/jobs/launch/readPLColors), using that admin's own stored GitHub PAT.
22
 *
23
 * <p>PrairieLearn does not expose these colors through an API, so this job reads the raw {@code
24
 * colors.scss} stylesheet from the public PrairieLearn repo and locates the {@code $custom-colors:
25
 * ( 'name': #hex, ... )} Sass map with a regular expression, rather than a full SCSS parser.
26
 */
27
@Builder
28
public class ReadPLColorsJob implements JobContextConsumer {
29
30
  static final String PL_REPO_NAME = "PrairieLearn/PrairieLearn";
31
  static final String COLORS_SCSS_PATH = "apps/prairielearn/public/stylesheets/colors.scss";
32
33
  // Matches the $custom-colors: ( ... ); Sass map as a whole, non-greedily, so that only the
34
  // color map's own entries (and not any later parentheses in the file) are captured.
35
  static final Pattern COLOR_MAP_PATTERN =
36
      Pattern.compile("\\$custom-colors\\s*:\\s*\\((.*?)\\)\\s*;", Pattern.DOTALL);
37
38
  // Matches a single 'name': #hex entry within the color map.
39
  static final Pattern COLOR_ENTRY_PATTERN =
40
      Pattern.compile("['\"]([a-zA-Z0-9_-]+)['\"]\\s*:\\s*#([0-9a-fA-F]{3,8})");
41
42
  private long userId;
43
  private PatCredentialRepository patCredentialRepository;
44
  private PatEncryptionService patEncryptionService;
45
  private GithubService githubService;
46
  private PlColorRepository plColorRepository;
47
48
  @Override
49
  public void accept(JobContext ctx) throws Exception {
50 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
    ctx.log("Reading PrairieLearn colors from %s".formatted(COLORS_SCSS_PATH));
51
52
    Optional<PatCredential> githubCredential =
53
        patCredentialRepository.findByUserIdAndPlatform(userId, PatPlatform.GITHUB);
54 1 1. accept : negated conditional → KILLED
    if (githubCredential.isEmpty()) {
55
      String message =
56
          "No GitHub PAT is set up for this user; set one up on the /profile page before running"
57
              + " this job";
58 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      ctx.log(message);
59
      throw new Exception(message);
60
    }
61
    String githubToken =
62
        patEncryptionService.decrypt(
63
            githubCredential.get().getCiphertext(), githubCredential.get().getKeyVersion());
64
65
    String scss;
66
    try {
67
      scss = githubService.getFileContent(PL_REPO_NAME, COLORS_SCSS_PATH, githubToken);
68
    } catch (HttpClientErrorException e) {
69
      String message =
70
          "Could not read %s from %s (HTTP %d); the file may not exist or may not be accessible"
71
              .formatted(COLORS_SCSS_PATH, PL_REPO_NAME, e.getStatusCode().value());
72 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      ctx.log(message);
73
      throw new Exception(message);
74
    }
75
76 1 1. accept : negated conditional → KILLED
    if (scss.isBlank()) {
77
      String message =
78
          "%s in %s is empty or could not be read".formatted(COLORS_SCSS_PATH, PL_REPO_NAME);
79 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      ctx.log(message);
80
      throw new Exception(message);
81
    }
82
83
    Matcher mapMatcher = COLOR_MAP_PATTERN.matcher(scss);
84 1 1. accept : negated conditional → KILLED
    if (!mapMatcher.find()) {
85
      String message =
86
          "%s does not contain the expected $custom-colors color mapping"
87
              .formatted(COLORS_SCSS_PATH);
88 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      ctx.log(message);
89
      throw new Exception(message);
90
    }
91
92
    String colorMapBody = mapMatcher.group(1);
93
    Matcher entryMatcher = COLOR_ENTRY_PATTERN.matcher(colorMapBody);
94
95
    int added = 0;
96
    int updated = 0;
97
    int unchanged = 0;
98
    boolean anyEntries = false;
99 1 1. accept : negated conditional → KILLED
    while (entryMatcher.find()) {
100
      anyEntries = true;
101
      String colorName = entryMatcher.group(1);
102
      String hexCode = "#" + entryMatcher.group(2).toLowerCase();
103
104
      Optional<PlColor> existing = plColorRepository.findById(colorName);
105 1 1. accept : negated conditional → KILLED
      if (existing.isEmpty()) {
106
        plColorRepository.save(PlColor.builder().colorName(colorName).hexCode(hexCode).build());
107 1 1. accept : Changed increment from 1 to -1 → KILLED
        added++;
108 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
        ctx.log("Added color %s: %s".formatted(colorName, hexCode));
109 1 1. accept : negated conditional → KILLED
      } else if (!existing.get().getHexCode().equalsIgnoreCase(hexCode)) {
110
        PlColor color = existing.get();
111
        String oldHexCode = color.getHexCode();
112 1 1. accept : removed call to edu/ucsb/cs/scaffold/entity/PlColor::setHexCode → KILLED
        color.setHexCode(hexCode);
113
        plColorRepository.save(color);
114 1 1. accept : Changed increment from 1 to -1 → KILLED
        updated++;
115 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
        ctx.log("Updated color %s: %s -> %s".formatted(colorName, oldHexCode, hexCode));
116
      } else {
117 1 1. accept : Changed increment from 1 to -1 → KILLED
        unchanged++;
118
      }
119
    }
120
121 1 1. accept : negated conditional → KILLED
    if (!anyEntries) {
122
      String message =
123
          "%s did not contain any 'name': #hex color entries".formatted(COLORS_SCSS_PATH);
124 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
      ctx.log(message);
125
      throw new Exception(message);
126
    }
127
128 1 1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED
    ctx.log("Done: %d added, %d updated, %d unchanged".formatted(added, updated, unchanged));
129
  }
130
}

Mutations

50

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

54

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_no_github_pat_is_configured()]
negated conditional → KILLED

58

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_no_github_pat_is_configured()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

72

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_colors_scss_file_cannot_be_read()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

76

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_file_content_is_blank()]
negated conditional → KILLED

79

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_file_content_is_blank()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

84

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_color_map_is_found_but_has_no_entries()]
negated conditional → KILLED

88

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_file_does_not_contain_the_color_mapping()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

99

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_color_map_is_found_but_has_no_entries()]
negated conditional → KILLED

105

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
negated conditional → KILLED

107

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
Changed increment from 1 to -1 → KILLED

108

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

109

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
negated conditional → KILLED

112

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
removed call to edu/ucsb/cs/scaffold/entity/PlColor::setHexCode → KILLED

114

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
Changed increment from 1 to -1 → KILLED

115

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

117

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
Changed increment from 1 to -1 → KILLED

121

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_color_map_is_found_but_has_no_entries()]
negated conditional → KILLED

124

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:fails_when_the_color_map_is_found_but_has_no_entries()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

128

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.ReadPLColorsJobTests]/[method:adds_new_colors_and_updates_changed_hex_codes_and_leaves_unchanged_ones_alone()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0