selective-ruby-core 0.2.8-aarch64-linux → 0.2.10-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +0 -0
- data/Rakefile +20 -0
- data/ext/selective_tracer/extconf.rb +41 -0
- data/ext/selective_tracer/selective_tracer.c +336 -0
- data/lib/bin/build_env.sh +115 -2
- data/lib/selective/ruby/core/controller.rb +77 -7
- data/lib/selective/ruby/core/file_correlator.rb +0 -0
- data/lib/selective/ruby/core/helper.rb +0 -0
- data/lib/selective/ruby/core/named_pipe.rb +0 -0
- data/lib/selective/ruby/core/test_map/recorder.rb +61 -0
- data/lib/selective/ruby/core/test_map/tracer.rb +120 -0
- data/lib/selective/ruby/core/test_map/tree.rb +125 -0
- data/lib/selective/ruby/core/test_map/uploader.rb +108 -0
- data/lib/selective/ruby/core/test_map.rb +89 -0
- data/lib/selective/ruby/core/version.rb +1 -1
- data/lib/selective-ruby-core.rb +0 -0
- data/selective-ruby-core.gemspec +53 -0
- metadata +29 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52b12017e465ee18c005f6661d79d12ea3e23821111ebe8ce13999b88dbc93b3
|
|
4
|
+
data.tar.gz: b1c4bbae8c2c5eb6dbc22376d8c1b6fbd49441af39de0632489e49b448692821
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '028d9d8ddbebf24ba288ea081d6cc2d06f8cca626258c514d120e0f8140578e9e92b54c4dcbb281c2734844ec7a2fb622346eef84ab8e5867602efa022266d5a'
|
|
7
|
+
data.tar.gz: 7edf81122e5c5978f16866b5b1848bfe7514762e7831bc7592b3311c1524727b4df29ba49c74f2f8e6e073b77b30464fae5ccd36a7a9f9afc5ade85b04dc340b
|
data/LICENSE
CHANGED
|
File without changes
|
data/Rakefile
CHANGED
|
@@ -5,6 +5,26 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
+
# Builds the optional test map tracer into lib/ for development and CI.
|
|
9
|
+
# Installed gems build it through `spec.extensions` instead.
|
|
10
|
+
desc "Compile the native test map tracer"
|
|
11
|
+
task :compile do
|
|
12
|
+
require "fileutils"
|
|
13
|
+
require "rbconfig"
|
|
14
|
+
ext_dir = File.expand_path("ext/selective_tracer", __dir__)
|
|
15
|
+
build_dir = File.expand_path("tmp/selective_tracer", __dir__)
|
|
16
|
+
FileUtils.mkdir_p(build_dir)
|
|
17
|
+
Dir.chdir(build_dir) do
|
|
18
|
+
# extconf must not inherit this process's bundle; it only needs mkmf.
|
|
19
|
+
Bundler.with_unbundled_env do
|
|
20
|
+
sh RbConfig.ruby, File.join(ext_dir, "extconf.rb")
|
|
21
|
+
sh "make"
|
|
22
|
+
end
|
|
23
|
+
built = Dir["selective_tracer.{#{RbConfig::CONFIG["DLEXT"]},bundle,so}"].first
|
|
24
|
+
FileUtils.cp(built, File.expand_path("lib", __dir__)) if built
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
8
28
|
require "standard/rake"
|
|
9
29
|
|
|
10
30
|
task default: %i[spec standard]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The tracer is optional. A customer without a C toolchain (or on an
|
|
4
|
+
# unsupported Ruby) must still be able to install selective-ruby-core and run
|
|
5
|
+
# their suite exactly as before, so any failed check writes a no-op Makefile
|
|
6
|
+
# instead of failing `gem install`. That includes compiling the tracer itself
|
|
7
|
+
# here first: an error in `make` would fail the install, so a Ruby whose
|
|
8
|
+
# headers the source doesn't build against must be caught before it. At
|
|
9
|
+
# runtime the missing library simply means "this runner can't record a test
|
|
10
|
+
# map".
|
|
11
|
+
|
|
12
|
+
require "mkmf"
|
|
13
|
+
|
|
14
|
+
def write_noop_makefile(reason)
|
|
15
|
+
warn "selective_tracer: skipping native build (#{reason}); test map recording will be unavailable"
|
|
16
|
+
File.write("Makefile", <<~MAKE)
|
|
17
|
+
all:
|
|
18
|
+
\t@true
|
|
19
|
+
install:
|
|
20
|
+
\t@true
|
|
21
|
+
clean:
|
|
22
|
+
\t@true
|
|
23
|
+
MAKE
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
if RUBY_ENGINE != "ruby"
|
|
28
|
+
write_noop_makefile("#{RUBY_ENGINE} is not supported")
|
|
29
|
+
elsif !have_header("ruby/debug.h") || !have_func("rb_profile_frames", "ruby/debug.h")
|
|
30
|
+
write_noop_makefile("required VM hook APIs are missing")
|
|
31
|
+
else
|
|
32
|
+
$CFLAGS << " -O2 -std=c99 -Wall -Wno-unused-parameter" # standard:disable Style/GlobalVars -- mkmf's interface
|
|
33
|
+
if try_compile(File.read(File.expand_path("selective_tracer.c", __dir__)))
|
|
34
|
+
create_makefile("selective_tracer")
|
|
35
|
+
else
|
|
36
|
+
write_noop_makefile("the tracer does not compile on Ruby #{RUBY_VERSION}; see mkmf.log")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
rescue => e
|
|
40
|
+
write_noop_makefile(e.message)
|
|
41
|
+
end
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// selective_tracer — records which source files a single test executes.
|
|
2
|
+
//
|
|
3
|
+
// A test map is built from one question asked of every test: "which files
|
|
4
|
+
// did you run?". This extension answers it with two raw VM event hooks that
|
|
5
|
+
// never call back into Ruby while the test runs:
|
|
6
|
+
//
|
|
7
|
+
// * RUBY_EVENT_LINE | CALL | B_CALL, registered globally so work done on
|
|
8
|
+
// *any* thread is
|
|
9
|
+
// attributed to the running test. Capybara drives a Puma server thread;
|
|
10
|
+
// a hook scoped to the test's own thread (what rotoscope does) would map
|
|
11
|
+
// every system test to nothing and let selection skip them wrongly.
|
|
12
|
+
// Selective runs one test at a time per process, so "everything that ran
|
|
13
|
+
// between start and stop" is the right attribution. Stray background
|
|
14
|
+
// threads can only add files, which makes selection more conservative,
|
|
15
|
+
// never less. CALL and B_CALL are not redundant with LINE: an endless
|
|
16
|
+
// method (`def total = price * qty`) and a one-line block executed from
|
|
17
|
+
// another file emit no line event of their own, so a line-only hook never
|
|
18
|
+
// ties a test to the file they live in.
|
|
19
|
+
//
|
|
20
|
+
// * RUBY_INTERNAL_EVENT_NEWOBJ (optional), which records the class of every
|
|
21
|
+
// plain object allocated. A class whose file only ran at boot — an
|
|
22
|
+
// ActiveRecord model with no methods of its own — executes no lines
|
|
23
|
+
// during a test, so line events alone would never tie the test to it.
|
|
24
|
+
// The classes are resolved to source files in Ruby after the test stops.
|
|
25
|
+
//
|
|
26
|
+
// Why not Ruby's Coverage module: it is process-global, so it cannot run next
|
|
27
|
+
// to SimpleCov, and while it is enabled Bootsnap cannot use its compile cache.
|
|
28
|
+
// Why not TracePoint from Ruby: every event re-enters the interpreter. The
|
|
29
|
+
// design follows what Datadog learned building datadog-ci-rb (line hook with
|
|
30
|
+
// pointer caching, allocation tracking for code-less classes), written for
|
|
31
|
+
// Selective's needs: several project roots, several ignored prefixes, and no
|
|
32
|
+
// Ruby API use inside the allocation hook.
|
|
33
|
+
|
|
34
|
+
#include <ruby.h>
|
|
35
|
+
#include <ruby/debug.h>
|
|
36
|
+
#include <ruby/st.h>
|
|
37
|
+
|
|
38
|
+
#include <stdbool.h>
|
|
39
|
+
#include <string.h>
|
|
40
|
+
|
|
41
|
+
#define PATH_CACHE_SIZE 1024 // power of two
|
|
42
|
+
#define KLASS_CACHE_SIZE 4096 // power of two
|
|
43
|
+
#define MAX_PREFIXES 32
|
|
44
|
+
#define EXEC_EVENTS (RUBY_EVENT_LINE | RUBY_EVENT_CALL | RUBY_EVENT_B_CALL)
|
|
45
|
+
|
|
46
|
+
struct prefix_list {
|
|
47
|
+
int count;
|
|
48
|
+
char *values[MAX_PREFIXES];
|
|
49
|
+
long lengths[MAX_PREFIXES];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
struct tracer {
|
|
53
|
+
// { path(String) => true } for the test being recorded.
|
|
54
|
+
VALUE files;
|
|
55
|
+
|
|
56
|
+
// Absolute paths are kept only under one of `roots` and outside every
|
|
57
|
+
// `ignored` prefix (bundled gems, Selective's own gems, tmp dirs).
|
|
58
|
+
struct prefix_list roots;
|
|
59
|
+
struct prefix_list ignored;
|
|
60
|
+
|
|
61
|
+
// Consecutive line events almost always come from the same file, and the
|
|
62
|
+
// path pointer of an iseq is stable, so a pointer comparison settles most
|
|
63
|
+
// events. The strings are marked so a freed path's address can't be reused
|
|
64
|
+
// for a different file and wrongly treated as "already seen".
|
|
65
|
+
VALUE last_path;
|
|
66
|
+
VALUE path_cache[PATH_CACHE_SIZE];
|
|
67
|
+
|
|
68
|
+
bool allocations;
|
|
69
|
+
bool running;
|
|
70
|
+
|
|
71
|
+
// Classes allocated during the test. Keys are class VALUEs, pinned by mark.
|
|
72
|
+
st_table *klasses;
|
|
73
|
+
VALUE last_klass;
|
|
74
|
+
VALUE klass_cache[KLASS_CACHE_SIZE];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
static int mark_klass_i(st_data_t key, st_data_t _value, st_data_t _arg) {
|
|
78
|
+
rb_gc_mark((VALUE)key);
|
|
79
|
+
return ST_CONTINUE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static void tracer_mark(void *ptr) {
|
|
83
|
+
struct tracer *t = ptr;
|
|
84
|
+
rb_gc_mark(t->files);
|
|
85
|
+
rb_gc_mark(t->last_path);
|
|
86
|
+
for (int i = 0; i < PATH_CACHE_SIZE; i++) rb_gc_mark(t->path_cache[i]);
|
|
87
|
+
rb_gc_mark(t->last_klass);
|
|
88
|
+
for (int i = 0; i < KLASS_CACHE_SIZE; i++) rb_gc_mark(t->klass_cache[i]);
|
|
89
|
+
if (t->klasses) st_foreach(t->klasses, mark_klass_i, 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static void free_prefixes(struct prefix_list *list) {
|
|
93
|
+
for (int i = 0; i < list->count; i++) xfree(list->values[i]);
|
|
94
|
+
list->count = 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static void tracer_free(void *ptr) {
|
|
98
|
+
struct tracer *t = ptr;
|
|
99
|
+
// No hook cleanup needed: the VM marks every registered hook's data, so a
|
|
100
|
+
// tracer that is still running is reachable and can't be freed.
|
|
101
|
+
free_prefixes(&t->roots);
|
|
102
|
+
free_prefixes(&t->ignored);
|
|
103
|
+
if (t->klasses) st_free_table(t->klasses);
|
|
104
|
+
xfree(t);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static size_t tracer_memsize(const void *ptr) {
|
|
108
|
+
return sizeof(struct tracer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static const rb_data_type_t tracer_type = {
|
|
112
|
+
.wrap_struct_name = "selective_tracer",
|
|
113
|
+
.function = {.dmark = tracer_mark, .dfree = tracer_free, .dsize = tracer_memsize},
|
|
114
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY};
|
|
115
|
+
|
|
116
|
+
static void reset_caches(struct tracer *t) {
|
|
117
|
+
t->last_path = Qnil;
|
|
118
|
+
for (int i = 0; i < PATH_CACHE_SIZE; i++) t->path_cache[i] = Qnil;
|
|
119
|
+
t->last_klass = Qnil;
|
|
120
|
+
for (int i = 0; i < KLASS_CACHE_SIZE; i++) t->klass_cache[i] = Qnil;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static VALUE tracer_alloc(VALUE klass) {
|
|
124
|
+
struct tracer *t;
|
|
125
|
+
VALUE self = TypedData_Make_Struct(klass, struct tracer, &tracer_type, t);
|
|
126
|
+
t->files = Qnil;
|
|
127
|
+
t->roots.count = 0;
|
|
128
|
+
t->ignored.count = 0;
|
|
129
|
+
t->allocations = false;
|
|
130
|
+
t->running = false;
|
|
131
|
+
t->klasses = NULL;
|
|
132
|
+
reset_caches(t);
|
|
133
|
+
t->files = rb_hash_new();
|
|
134
|
+
t->klasses = st_init_numtable();
|
|
135
|
+
return self;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static struct tracer *get_tracer(VALUE self) {
|
|
139
|
+
struct tracer *t;
|
|
140
|
+
TypedData_Get_Struct(self, struct tracer, &tracer_type, t);
|
|
141
|
+
return t;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static bool has_dir_prefix(const char *path, long path_len, const char *prefix, long prefix_len) {
|
|
145
|
+
if (prefix_len > path_len || memcmp(prefix, path, prefix_len) != 0) return false;
|
|
146
|
+
// "/app" must not match "/application"; a prefix given with a trailing
|
|
147
|
+
// slash already carries the boundary.
|
|
148
|
+
return prefix_len == path_len || prefix[prefix_len - 1] == '/' || path[prefix_len] == '/';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static bool path_included(struct tracer *t, const char *path, long len) {
|
|
152
|
+
if (len == 0) return false;
|
|
153
|
+
|
|
154
|
+
if (path[0] != '/') {
|
|
155
|
+
// Relative paths ("./spec/x_spec.rb" from `load`) are resolved and
|
|
156
|
+
// re-checked in Ruby. Pseudo-paths like "(eval at ...)" and
|
|
157
|
+
// "<internal:kernel>" are never project files.
|
|
158
|
+
return path[0] != '(' && path[0] != '<';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
bool in_root = false;
|
|
162
|
+
for (int i = 0; i < t->roots.count; i++) {
|
|
163
|
+
if (has_dir_prefix(path, len, t->roots.values[i], t->roots.lengths[i])) {
|
|
164
|
+
in_root = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!in_root) return false;
|
|
169
|
+
|
|
170
|
+
for (int i = 0; i < t->ignored.count; i++) {
|
|
171
|
+
if (has_dir_prefix(path, len, t->ignored.values[i], t->ignored.lengths[i])) return false;
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static inline size_t cache_slot(uintptr_t ptr, size_t size) {
|
|
177
|
+
return ((ptr >> 4) ^ (ptr >> 12)) & (size - 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static void on_exec(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass) {
|
|
181
|
+
struct tracer *t = RTYPEDDATA_DATA(data);
|
|
182
|
+
|
|
183
|
+
const char *current = rb_sourcefile();
|
|
184
|
+
if (current == NULL) return;
|
|
185
|
+
|
|
186
|
+
if (t->last_path != Qnil && RSTRING_PTR(t->last_path) == current) return;
|
|
187
|
+
|
|
188
|
+
size_t slot = cache_slot((uintptr_t)current, PATH_CACHE_SIZE);
|
|
189
|
+
VALUE cached = t->path_cache[slot];
|
|
190
|
+
if (cached != Qnil && RSTRING_PTR(cached) == current) {
|
|
191
|
+
t->last_path = cached;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// First time this test sees this file: fetch the frame's path String so
|
|
196
|
+
// the pointer we cache is owned by a marked object.
|
|
197
|
+
VALUE frame;
|
|
198
|
+
if (rb_profile_frames(0, 1, &frame, NULL) != 1) return;
|
|
199
|
+
|
|
200
|
+
VALUE path = rb_profile_frame_path(frame);
|
|
201
|
+
if (NIL_P(path) || !RB_TYPE_P(path, T_STRING)) return;
|
|
202
|
+
|
|
203
|
+
t->last_path = path;
|
|
204
|
+
t->path_cache[slot] = path;
|
|
205
|
+
|
|
206
|
+
if (path_included(t, RSTRING_PTR(path), RSTRING_LEN(path))) {
|
|
207
|
+
rb_hash_aset(t->files, path, Qtrue);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Runs inside object allocation: no Ruby method calls and no Ruby object
|
|
212
|
+
// allocation are allowed here. rb_obj_class, rb_mod_name and st_* do neither.
|
|
213
|
+
static void on_newobj(VALUE data, rb_trace_arg_t *targ) {
|
|
214
|
+
VALUE obj = rb_tracearg_object(targ);
|
|
215
|
+
enum ruby_value_type type = rb_type(obj);
|
|
216
|
+
if (type != RUBY_T_OBJECT && type != RUBY_T_STRUCT) return;
|
|
217
|
+
|
|
218
|
+
VALUE klass = rb_obj_class(obj);
|
|
219
|
+
if (klass == 0 || NIL_P(klass)) return;
|
|
220
|
+
|
|
221
|
+
struct tracer *t = RTYPEDDATA_DATA(data);
|
|
222
|
+
if (t->last_klass == klass) return;
|
|
223
|
+
|
|
224
|
+
size_t slot = cache_slot((uintptr_t)klass, KLASS_CACHE_SIZE);
|
|
225
|
+
if (t->klass_cache[slot] == klass) {
|
|
226
|
+
t->last_klass = klass;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
t->last_klass = klass;
|
|
231
|
+
t->klass_cache[slot] = klass;
|
|
232
|
+
|
|
233
|
+
if (st_is_member(t->klasses, (st_data_t)klass)) return;
|
|
234
|
+
if (NIL_P(rb_mod_name(klass))) return; // anonymous class: no file to resolve
|
|
235
|
+
|
|
236
|
+
st_insert(t->klasses, (st_data_t)klass, 1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static void load_prefixes(struct prefix_list *list, VALUE ary, const char *name) {
|
|
240
|
+
if (NIL_P(ary)) return;
|
|
241
|
+
Check_Type(ary, T_ARRAY);
|
|
242
|
+
long n = RARRAY_LEN(ary);
|
|
243
|
+
if (n > MAX_PREFIXES) rb_raise(rb_eArgError, "at most %d %s are supported", MAX_PREFIXES, name);
|
|
244
|
+
|
|
245
|
+
for (long i = 0; i < n; i++) {
|
|
246
|
+
VALUE s = rb_ary_entry(ary, i);
|
|
247
|
+
Check_Type(s, T_STRING);
|
|
248
|
+
long len = RSTRING_LEN(s);
|
|
249
|
+
if (len == 0) continue;
|
|
250
|
+
char *copy = ALLOC_N(char, len + 1);
|
|
251
|
+
memcpy(copy, RSTRING_PTR(s), len);
|
|
252
|
+
copy[len] = '\0';
|
|
253
|
+
list->values[list->count] = copy;
|
|
254
|
+
list->lengths[list->count] = len;
|
|
255
|
+
list->count++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// NativeTracer.new(roots: [...], ignored: [...], allocations: true/false)
|
|
260
|
+
static VALUE tracer_initialize(int argc, VALUE *argv, VALUE self) {
|
|
261
|
+
VALUE opts;
|
|
262
|
+
rb_scan_args(argc, argv, "1", &opts);
|
|
263
|
+
Check_Type(opts, T_HASH);
|
|
264
|
+
|
|
265
|
+
struct tracer *t = get_tracer(self);
|
|
266
|
+
free_prefixes(&t->roots);
|
|
267
|
+
free_prefixes(&t->ignored);
|
|
268
|
+
|
|
269
|
+
load_prefixes(&t->roots, rb_hash_aref(opts, ID2SYM(rb_intern("roots"))), "roots");
|
|
270
|
+
load_prefixes(&t->ignored, rb_hash_aref(opts, ID2SYM(rb_intern("ignored"))), "ignored prefixes");
|
|
271
|
+
if (t->roots.count == 0) rb_raise(rb_eArgError, "roots must contain at least one path");
|
|
272
|
+
|
|
273
|
+
t->allocations = RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("allocations"))));
|
|
274
|
+
return self;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
static VALUE tracer_start(VALUE self) {
|
|
278
|
+
struct tracer *t = get_tracer(self);
|
|
279
|
+
if (t->running) rb_raise(rb_eRuntimeError, "tracer is already running");
|
|
280
|
+
|
|
281
|
+
rb_hash_clear(t->files);
|
|
282
|
+
st_clear(t->klasses);
|
|
283
|
+
reset_caches(t);
|
|
284
|
+
|
|
285
|
+
rb_add_event_hook2((rb_event_hook_func_t)on_exec, EXEC_EVENTS, self, RUBY_EVENT_HOOK_FLAG_SAFE);
|
|
286
|
+
if (t->allocations) {
|
|
287
|
+
rb_add_event_hook2((rb_event_hook_func_t)on_newobj, RUBY_INTERNAL_EVENT_NEWOBJ, self,
|
|
288
|
+
RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG);
|
|
289
|
+
}
|
|
290
|
+
t->running = true;
|
|
291
|
+
return self;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static int collect_klass_i(st_data_t key, st_data_t _value, st_data_t ary) {
|
|
295
|
+
rb_ary_push((VALUE)ary, (VALUE)key);
|
|
296
|
+
return ST_CONTINUE;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Returns [files, classes]: the Hash of executed paths and the Array of named
|
|
300
|
+
// classes allocated during the test. Both are fresh objects owned by Ruby.
|
|
301
|
+
static VALUE tracer_stop(VALUE self) {
|
|
302
|
+
struct tracer *t = get_tracer(self);
|
|
303
|
+
if (!t->running) rb_raise(rb_eRuntimeError, "tracer is not running");
|
|
304
|
+
|
|
305
|
+
rb_remove_event_hook_with_data((rb_event_hook_func_t)on_exec, self);
|
|
306
|
+
if (t->allocations) rb_remove_event_hook_with_data((rb_event_hook_func_t)on_newobj, self);
|
|
307
|
+
t->running = false;
|
|
308
|
+
|
|
309
|
+
VALUE files = t->files;
|
|
310
|
+
t->files = rb_hash_new();
|
|
311
|
+
|
|
312
|
+
VALUE klasses = rb_ary_new_capa((long)t->klasses->num_entries);
|
|
313
|
+
st_foreach(t->klasses, collect_klass_i, (st_data_t)klasses);
|
|
314
|
+
st_clear(t->klasses);
|
|
315
|
+
reset_caches(t);
|
|
316
|
+
|
|
317
|
+
return rb_assoc_new(files, klasses);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static VALUE tracer_running_p(VALUE self) {
|
|
321
|
+
return get_tracer(self)->running ? Qtrue : Qfalse;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
void Init_selective_tracer(void) {
|
|
325
|
+
VALUE mSelective = rb_define_module("Selective");
|
|
326
|
+
VALUE mRuby = rb_define_module_under(mSelective, "Ruby");
|
|
327
|
+
VALUE mCore = rb_define_module_under(mRuby, "Core");
|
|
328
|
+
VALUE mTestMap = rb_define_module_under(mCore, "TestMap");
|
|
329
|
+
VALUE cTracer = rb_define_class_under(mTestMap, "NativeTracer", rb_cObject);
|
|
330
|
+
|
|
331
|
+
rb_define_alloc_func(cTracer, tracer_alloc);
|
|
332
|
+
rb_define_method(cTracer, "initialize", tracer_initialize, -1);
|
|
333
|
+
rb_define_method(cTracer, "start", tracer_start, 0);
|
|
334
|
+
rb_define_method(cTracer, "stop", tracer_stop, 0);
|
|
335
|
+
rb_define_method(cTracer, "running?", tracer_running_p, 0);
|
|
336
|
+
}
|
data/lib/bin/build_env.sh
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Parse an "owner/repo" slug out of a git remote URL (SSH or HTTPS) that points
|
|
4
|
+
# at github.com. Emits an empty string when the URL is not a github.com URL.
|
|
5
|
+
parse_github_slug() {
|
|
6
|
+
local url="$1"
|
|
7
|
+
case "$url" in
|
|
8
|
+
git@github.com:*|git://github.com/*|https://github.com/*|http://github.com/*|ssh://git@github.com/*)
|
|
9
|
+
echo "$url" | sed -E 's#^(git@|(git|https?|ssh)://(git@)?)github\.com[:/]##; s#\.git/?$##'
|
|
10
|
+
;;
|
|
11
|
+
esac
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Given a git remote URL, classify the provider as github|gitlab|bitbucket or
|
|
15
|
+
# empty when unrecognized. Used for the git_provider hint.
|
|
16
|
+
detect_git_provider_from_url() {
|
|
17
|
+
local url="$1"
|
|
18
|
+
case "$url" in
|
|
19
|
+
*github.com*) echo "github" ;;
|
|
20
|
+
*gitlab.com*) echo "gitlab" ;;
|
|
21
|
+
*bitbucket.org*) echo "bitbucket" ;;
|
|
22
|
+
esac
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Detect the platform
|
|
4
26
|
if [ -n "$GITHUB_ACTIONS" ]; then
|
|
5
27
|
platform=github_actions
|
|
6
28
|
branch=${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}
|
|
@@ -13,6 +35,24 @@ if [ -n "$GITHUB_ACTIONS" ]; then
|
|
|
13
35
|
commit_message=$(git log --format=%s -n 1 $sha)
|
|
14
36
|
committer_name=$(git show -s --format='%an' -n 1 $sha)
|
|
15
37
|
committer_email=$(git show -s --format='%ae' -n 1 $sha)
|
|
38
|
+
git_repo_full_name=$GITHUB_REPOSITORY
|
|
39
|
+
git_provider=github
|
|
40
|
+
# Extract base_sha from the webhook event payload GitHub drops at
|
|
41
|
+
# $GITHUB_EVENT_PATH. Prefer jq when available (handles both push and PR
|
|
42
|
+
# events); fall back to a grep-based parse for the top-level "before" field
|
|
43
|
+
# on push events when jq is missing.
|
|
44
|
+
if [ -f "$GITHUB_EVENT_PATH" ]; then
|
|
45
|
+
if command -v jq >/dev/null 2>&1; then
|
|
46
|
+
base_sha=$(jq -r '.pull_request.base.sha // .before // empty' < "$GITHUB_EVENT_PATH" 2>/dev/null)
|
|
47
|
+
else
|
|
48
|
+
base_sha=$(grep -o '"before"[[:space:]]*:[[:space:]]*"[^"]*"' "$GITHUB_EVENT_PATH" | head -1 | sed -E 's/.*"[^"]*"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')
|
|
49
|
+
fi
|
|
50
|
+
# GitHub emits all-zero SHAs when the ref was just created and has no prior
|
|
51
|
+
# state. Treat that as "no base" so the server falls through to head-only.
|
|
52
|
+
if [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
53
|
+
base_sha=""
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
16
56
|
elif [ -n "$CIRCLECI" ]; then
|
|
17
57
|
platform=circleci
|
|
18
58
|
branch=$CIRCLE_BRANCH
|
|
@@ -23,6 +63,19 @@ elif [ -n "$CIRCLECI" ]; then
|
|
|
23
63
|
commit_message=$(git log --format=%s -n 1 $sha)
|
|
24
64
|
committer_name=$(git show -s --format='%an' -n 1 $sha)
|
|
25
65
|
committer_email=$(git show -s --format='%ae' -n 1 $sha)
|
|
66
|
+
# CircleCI has no single "slug" env var. Compose from username + reponame.
|
|
67
|
+
# These work for both GitHub OAuth and GitHub App pipelines. For non-GitHub
|
|
68
|
+
# pipelines (GitLab, Bitbucket) the same vars exist but we gate on the repo
|
|
69
|
+
# URL to avoid emitting a non-GitHub slug.
|
|
70
|
+
if [ -n "$CIRCLE_PROJECT_USERNAME" ] && [ -n "$CIRCLE_PROJECT_REPONAME" ]; then
|
|
71
|
+
git_provider=$(detect_git_provider_from_url "$CIRCLE_REPOSITORY_URL")
|
|
72
|
+
if [ "$git_provider" = "github" ] || [ -z "$CIRCLE_REPOSITORY_URL" ]; then
|
|
73
|
+
git_repo_full_name="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}"
|
|
74
|
+
# If we fell through the "no repo URL" path, default provider to github
|
|
75
|
+
# since the slug composition path is specific to GitHub-style owner/repo.
|
|
76
|
+
[ -z "$git_provider" ] && git_provider=github
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
26
79
|
elif [ -n "$SEMAPHORE" ]; then
|
|
27
80
|
platform=semaphore
|
|
28
81
|
branch=${SEMAPHORE_GIT_PR_BRANCH:-$SEMAPHORE_GIT_BRANCH}
|
|
@@ -38,6 +91,35 @@ elif [ -n "$SEMAPHORE" ]; then
|
|
|
38
91
|
commit_message=$(git log --format=%s -n 1 $sha)
|
|
39
92
|
committer_name=$(git show -s --format='%an' -n 1 $sha)
|
|
40
93
|
committer_email=$(git show -s --format='%ae' -n 1 $sha)
|
|
94
|
+
git_provider=$SEMAPHORE_GIT_PROVIDER
|
|
95
|
+
if [ "$git_provider" = "github" ]; then
|
|
96
|
+
git_repo_full_name=$SEMAPHORE_GIT_REPO_SLUG
|
|
97
|
+
fi
|
|
98
|
+
# SEMAPHORE_GIT_COMMIT_RANGE is "<base>...<head>" on push/PR events; take the
|
|
99
|
+
# left side. It is unset when the build isn't commit-scoped.
|
|
100
|
+
if [ -n "$SEMAPHORE_GIT_COMMIT_RANGE" ]; then
|
|
101
|
+
base_sha=$(echo "$SEMAPHORE_GIT_COMMIT_RANGE" | sed -E 's/\.\.\..*//')
|
|
102
|
+
fi
|
|
103
|
+
elif [ -n "$BUILDKITE" ]; then
|
|
104
|
+
platform=buildkite
|
|
105
|
+
branch=$BUILDKITE_BRANCH
|
|
106
|
+
target_branch=$BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
|
107
|
+
actor=$BUILDKITE_BUILD_AUTHOR
|
|
108
|
+
sha=$BUILDKITE_COMMIT
|
|
109
|
+
run_id=$BUILDKITE_BUILD_ID
|
|
110
|
+
run_attempt=$BUILDKITE_RETRY_COUNT
|
|
111
|
+
runner_id=$BUILDKITE_PARALLEL_JOB
|
|
112
|
+
pr_title=$BUILDKITE_PULL_REQUEST_TITLE
|
|
113
|
+
commit_message=$BUILDKITE_MESSAGE
|
|
114
|
+
committer_name=$BUILDKITE_BUILD_AUTHOR
|
|
115
|
+
committer_email=$BUILDKITE_BUILD_AUTHOR_EMAIL
|
|
116
|
+
# Buildkite tells us the provider explicitly, and BUILDKITE_REPO is the URL.
|
|
117
|
+
if [ "$BUILDKITE_PIPELINE_PROVIDER" = "github" ]; then
|
|
118
|
+
git_provider=github
|
|
119
|
+
git_repo_full_name=$(parse_github_slug "$BUILDKITE_REPO")
|
|
120
|
+
else
|
|
121
|
+
git_provider=$BUILDKITE_PIPELINE_PROVIDER
|
|
122
|
+
fi
|
|
41
123
|
elif [ -n "$RWX" ]; then
|
|
42
124
|
platform=rwx
|
|
43
125
|
branch="${RWX_GIT_REF_NAME}"
|
|
@@ -52,6 +134,13 @@ elif [ -n "$RWX" ]; then
|
|
|
52
134
|
commit_message="${RWX_GIT_COMMIT_SUMMARY}"
|
|
53
135
|
committer_name="${RWX_GIT_COMMITTER_NAME}"
|
|
54
136
|
committer_email="${RWX_GIT_COMMITTER_EMAIL}"
|
|
137
|
+
# RWX_GIT_REPOSITORY_NAME is extracted by the git/clone package from whatever
|
|
138
|
+
# URL was cloned. For non-GitHub hosts this still looks like "owner/repo" but
|
|
139
|
+
# points elsewhere, so cross-check the URL host before trusting it.
|
|
140
|
+
git_provider=$(detect_git_provider_from_url "$RWX_GIT_REPOSITORY_URL")
|
|
141
|
+
if [ "$git_provider" = "github" ]; then
|
|
142
|
+
git_repo_full_name="${RWX_GIT_REPOSITORY_NAME}"
|
|
143
|
+
fi
|
|
55
144
|
elif [ -n "$MINT" ]; then
|
|
56
145
|
platform=rwx
|
|
57
146
|
branch="${MINT_GIT_REF_NAME}"
|
|
@@ -66,6 +155,27 @@ elif [ -n "$MINT" ]; then
|
|
|
66
155
|
commit_message="${MINT_GIT_COMMIT_SUMMARY}"
|
|
67
156
|
committer_name="${MINT_GIT_COMMITTER_NAME}"
|
|
68
157
|
committer_email="${MINT_GIT_COMMITTER_EMAIL}"
|
|
158
|
+
git_provider=$(detect_git_provider_from_url "$MINT_GIT_REPOSITORY_URL")
|
|
159
|
+
if [ "$git_provider" = "github" ]; then
|
|
160
|
+
git_repo_full_name="${MINT_GIT_REPOSITORY_NAME}"
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Final fallbacks when we haven't resolved the GitHub slug from a known CI
|
|
165
|
+
# platform. Order:
|
|
166
|
+
# 1. Explicit SELECTIVE_GIT_REPO override (always wins; see below).
|
|
167
|
+
# 2. git config --get remote.origin.url on a local checkout.
|
|
168
|
+
if [ -z "$git_repo_full_name" ] && command -v git >/dev/null 2>&1; then
|
|
169
|
+
origin_url=$(git config --get remote.origin.url 2>/dev/null || true)
|
|
170
|
+
if [ -n "$origin_url" ]; then
|
|
171
|
+
parsed_slug=$(parse_github_slug "$origin_url")
|
|
172
|
+
if [ -n "$parsed_slug" ]; then
|
|
173
|
+
git_repo_full_name="$parsed_slug"
|
|
174
|
+
[ -z "$git_provider" ] && git_provider=github
|
|
175
|
+
elif [ -z "$git_provider" ]; then
|
|
176
|
+
git_provider=$(detect_git_provider_from_url "$origin_url")
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
69
179
|
fi
|
|
70
180
|
|
|
71
181
|
function escape() {
|
|
@@ -83,11 +193,14 @@ cat <<EOF
|
|
|
83
193
|
"target_branch": "$(escape "${SELECTIVE_TARGET_BRANCH:-$target_branch}")",
|
|
84
194
|
"actor": "$(escape "${SELECTIVE_ACTOR:-$actor}")",
|
|
85
195
|
"sha": "$(escape "${SELECTIVE_SHA:-$sha}")",
|
|
196
|
+
"base_sha": "$(escape "${SELECTIVE_BASE_SHA:-$base_sha}")",
|
|
86
197
|
"run_id": "$(escape "${SELECTIVE_RUN_ID:-$run_id}")",
|
|
87
198
|
"run_attempt": "$(escape "${SELECTIVE_RUN_ATTEMPT:-$run_attempt}")",
|
|
88
199
|
"runner_id": "$(escape "${SELECTIVE_RUNNER_ID:-$runner_id}")",
|
|
89
200
|
"commit_message": "$(escape "${SELECTIVE_COMMIT_MESSAGE:-$commit_message}")",
|
|
90
201
|
"committer_name": "$(escape "${SELECTIVE_COMMITTER_NAME:-$committer_name}")",
|
|
91
|
-
"committer_email": "$(escape "${SELECTIVE_COMMITTER_EMAIL:-$committer_email}")"
|
|
202
|
+
"committer_email": "$(escape "${SELECTIVE_COMMITTER_EMAIL:-$committer_email}")",
|
|
203
|
+
"git_repo_full_name": "$(escape "${SELECTIVE_GIT_REPO:-$git_repo_full_name}")",
|
|
204
|
+
"git_provider": "$(escape "${SELECTIVE_GIT_PROVIDER:-$git_provider}")"
|
|
92
205
|
}
|
|
93
206
|
EOF
|