jsinstrument 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsinstrument.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mushan Yang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # JSInstrument
2
+
3
+ Instrument JavaScript source code to monitor code coverage in pure Ruby
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'jsinstrument'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install jsinstrument
18
+
19
+ ## Usage
20
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jsinstrument/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jsinstrument"
8
+ spec.version = JSInstrument::VERSION
9
+ spec.authors = ["Mushan Yang"]
10
+ spec.email = ["mushan.yang@hulu.com"]
11
+ spec.description = %q{Instrument JavaScript source code to monitor code coverage in pure Ruby}
12
+ spec.summary = %q{Instrument JavaScript source code to monitor code coverage in pure Ruby}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rkelly", "~>1.0.7"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,2 @@
1
+ require "jsinstrument/version"
2
+ require "jsinstrument/instrumenter"
@@ -0,0 +1,135 @@
1
+ /* automatically generated by JSInstrument */
2
+ /*
3
+ * create __coverage__ object as a global variable
4
+ */
5
+ !function(root){
6
+ if (root && (typeof root.%{js_object_name} === 'undefined')) {
7
+ root.%{js_object_name} = {
8
+ /**
9
+ * Coverage data
10
+ */
11
+ _data: {},
12
+ /**
13
+ * A thread is uploading data
14
+ */
15
+ _uploading: false,
16
+ /**
17
+ * mark code line
18
+ */
19
+ mark: function(file, lines) {
20
+ if(!this._data[file]) this._data[file] = [];
21
+ for (var i in lines) {
22
+ if(!this._data[file][lines[i]]) {
23
+ this._data[file][lines[i]] = 0;
24
+ }
25
+ }
26
+ },
27
+ /**
28
+ * Hit a line
29
+ */
30
+ hit: function(file, line) {
31
+ if(!this._data[file]) this._data[file] = [];
32
+
33
+ if(!this._data[file][line]) this._data[file][line] = 1;
34
+ else this._data[file][line] = this._data[file][line] + 1;
35
+ },
36
+ /**
37
+ * Push data to server
38
+ */
39
+ upload: function(force, mark) {
40
+ /**
41
+ * Avoid dup upload
42
+ */
43
+ if(!force && this._uploading) return;
44
+
45
+ this._uploading = true;
46
+
47
+ try {
48
+ $.post('%{upload_url}', this._serialize(this._data, mark)).success(function() {
49
+ for (var file in _$jscoverage._data) {
50
+ var length = _$jscoverage._data[file].length;
51
+ for (var line = 0; line < length; line++) {
52
+ var value = _$jscoverage._data[file][line];
53
+ if (value === undefined || value === null) continue;
54
+ _$jscoverage._data[file][line] = 0;
55
+ }
56
+ }
57
+ }).complete(function() {
58
+ _$jscoverage._uploading = false;
59
+ });
60
+ } catch (e) {
61
+ this._uploading = false;
62
+ }
63
+ }
64
+ /**
65
+ * serialize data for data uploading
66
+ */
67
+ _serialize: function(data, mark) {
68
+ var json = [];
69
+
70
+ for (var file in data) {
71
+ var coverage = data[file];
72
+
73
+ var array = [];
74
+ var length = coverage.length;
75
+ for (var line = 0; line < length; line++) {
76
+ var value = coverage[line];
77
+ if (value === undefined || value === null) {
78
+ value = '';
79
+ }
80
+ array.push(value);
81
+ }
82
+
83
+ json.push(this._quote(file) + ':[' + array.join(',') + ']');
84
+ }
85
+
86
+ var result = '{' + json.join(',') + '}';
87
+
88
+ var r = {
89
+ project: '%{project_name}',
90
+ commit: '%{commit_hash}',
91
+ batch: '%{batch_text}',
92
+ mark: mark,
93
+ data: result
94
+ }
95
+
96
+ var data_compress_method = %{data_compress_method};
97
+
98
+ if (data_compress_method) {
99
+ var result_deflated = data_compress_method(result);
100
+ var result_encoded = btoa(result_deflated);
101
+ r.data_compressed = true;
102
+ r.data = result_encoded;
103
+ }
104
+
105
+ return r;
106
+ },
107
+ _quote: function(s) {
108
+ return '"' + s.replace(/[\u0000-\u001f"\\\u007f-\uffff]/g, function (c) {
109
+ switch (c) {
110
+ case '\b': return '\\b';
111
+ case '\f': return '\\f';
112
+ case '\n': return '\\n';
113
+ case '\r': return '\\r';
114
+ case '\t': return '\\t';
115
+ case '"': return '\\"';
116
+ case '\\': return '\\\\';
117
+ default: return '\\u' + this._pad(c.charCodeAt(0).toString(16));
118
+ }
119
+ }) + '"';
120
+ },
121
+ _pad: function(s) {
122
+ return '0000'.substr(s.length) + s;
123
+ },
124
+ };
125
+
126
+ // $(document).ready(function() {
127
+ // $(window).unload(function (e) {
128
+ // _$jscoverage.upload(true);
129
+ // });
130
+ // });
131
+ }
132
+
133
+ root.%{js_object_name}.mark('%{src_filename}', %{instrumented_lines});
134
+ }(this);
135
+
@@ -0,0 +1,133 @@
1
+ require 'set'
2
+ require 'rkelly'
3
+ require 'rkelly/node'
4
+ require 'rkelly/parent_builder'
5
+ include RKelly::Nodes
6
+
7
+ module JSInstrument
8
+ class Instrumenter
9
+
10
+ attr_accessor :js_object_name, :upload_url, :project_name, :commit_hash, :batch_text, :data_compress_method
11
+
12
+ def js_object_name
13
+ @js_object_name || '__coverage__'
14
+ end
15
+
16
+ def instrument src, filename
17
+ raise 'Filename not set' unless filename
18
+ ast = RKelly::Parser.new.parse src
19
+ raise 'Parse source failed.' unless ast
20
+ ast, instrumented_lines = instrument_ast ast
21
+ instrumented_src = ast.to_ecma
22
+
23
+ inserting = File.open(File.dirname(__FILE__) + '/inserting.js').read
24
+
25
+ bindings = {
26
+ js_object_name: self.js_object_name,
27
+ upload_url: self.upload_url,
28
+ src_filename: self.filename,
29
+ project_name: self.project_name,
30
+ commit_hash: self.commit_hash,
31
+ batch_text: self.batch_text,
32
+ data_compress_method: self.data_compress_method,
33
+ instrumented_lines: instrumented_lines
34
+ }
35
+
36
+ (inserting % bindings) + instrumented_src
37
+ end
38
+
39
+ private
40
+ def instrument_ast ast
41
+ # build parent attr for all nodes in ast
42
+ #
43
+ ast = RKelly::Visitors::ParentBuilder.new.build ast
44
+ instrumented_lines = Set.new
45
+ ast.each do |node|
46
+ parent = node.parent
47
+ line = node.line
48
+ # instrument only if we can get the line no. and we haven't instrumented it
49
+ #
50
+ if line and not instrumented_lines.member? line
51
+ if ExpressionStatementNode === node or EmptyStatementNode === node or
52
+ DoWhileNode === node or WhileNode === node or ForNode === node or
53
+ ForInNode === node or ContinueNode === node or VarDeclNode === node or
54
+ SwitchNode === node or BreakNode === node or ThrowNode === node or
55
+ ReturnNode === node or IfNode === node or VarStatementNode === node
56
+
57
+ if DoWhileNode === parent
58
+ if parent.left.eql? node
59
+ instrumented_lines.add line
60
+ parent.left = get_inserted_block node
61
+ end
62
+ elsif IfNode === parent or WhileNode === parent or
63
+ ForNode === parent or ForInNode === parent or WithNode === parent
64
+ if parent.value.eql? node
65
+ instrumented_lines.add line
66
+ parent.value = get_inserted_block node
67
+ elsif parent.else.eql? node
68
+ instrumented_lines.add line
69
+ parent.else = get_inserted_block node
70
+ end
71
+ elsif CaseBlockNode === parent or LabelNode === parent
72
+ # do nothing here
73
+ elsif SourceElementsNode === parent
74
+ if insert_func_before node
75
+ instrumented_lines.add line
76
+ end
77
+ end
78
+ elsif WithNode === node or TryNode === node
79
+ if SourceElementsNode === parent
80
+ if insert_func_before node
81
+ instrumented_lines.add line
82
+ end
83
+ end
84
+ elsif FunctionDeclNode === node or FunctionExprNode === node
85
+ if insert_func_before node
86
+ instrumented_lines.add line
87
+ end
88
+ end
89
+ end
90
+ end
91
+ [ast, instrumented_lines.sort]
92
+ end
93
+
94
+ def get_insert_func_call line
95
+ ExpressionStatementNode.new(
96
+ FunctionCallNode.new(
97
+ ResolveNode.new("#{self.js_object_name}.hit"),
98
+ ArgumentsNode.new([
99
+ StringNode.new("\"#{self.src_filename}\""),
100
+ NumberNode.new(line)
101
+ ])
102
+ )
103
+ )
104
+ end
105
+
106
+ def get_inserted_block node
107
+ BlockNode.new(
108
+ SourceElementsNode.new([
109
+ get_insert_func_call(node.line),
110
+ node
111
+ ])
112
+ )
113
+ end
114
+
115
+ def insert_func_before node
116
+ # get the original node line
117
+ #
118
+ line = node.line
119
+ # backtrack until we can insert the func
120
+ #
121
+ while not SourceElementsNode === node.parent and node.parent != nil
122
+ node = node.parent
123
+ end
124
+ if node.parent == nil
125
+ false
126
+ else
127
+ index = node.parent.value.index{|x| x.eql? node}
128
+ node.parent.value.insert index, get_insert_func_call(line)
129
+ true
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,3 @@
1
+ module JSInstrument
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,14 @@
1
+ module RKelly
2
+ module Nodes
3
+ class Node
4
+ # add an accessor to receord the parent of a node
5
+ #
6
+ attr_accessor :parent
7
+ end
8
+ class IfNode
9
+ # make the else part of IfNode writable
10
+ #
11
+ attr_accessor :else
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,120 @@
1
+ module RKelly
2
+ module Visitors
3
+ class ParentBuilder < Visitor
4
+
5
+ def build node
6
+ link_parents node, nil
7
+ node
8
+ end
9
+
10
+ ## define methods for traverse different kinds of nodes
11
+ ## consts are from RKelly::Visitors::Visitor
12
+
13
+ TERMINAL_NODES.each do |type|
14
+ define_method(:"visit_#{type}Node") { |o| o.value }
15
+ end
16
+
17
+ BINARY_NODES.each do |type|
18
+ define_method(:"visit_#{type}Node") do |o|
19
+ [o.left && link_parents(o.left, o), o.value && link_parents(o.value, o)]
20
+ end
21
+ end
22
+
23
+ ARRAY_VALUE_NODES.each do |type|
24
+ define_method(:"visit_#{type}Node") do |o|
25
+ o.value && o.value.map { |v| v ? link_parents(v, o) : nil }
26
+ end
27
+ end
28
+
29
+ NAME_VALUE_NODES.each do |type|
30
+ define_method(:"visit_#{type}Node") do |o|
31
+ [o.name.to_s.to_sym, o.value ? link_parents(o.value, o) : nil]
32
+ end
33
+ end
34
+
35
+ SINGLE_VALUE_NODES.each do |type|
36
+ define_method(:"visit_#{type}Node") do |o|
37
+ link_parents(o.value, o) if o.value
38
+ end
39
+ end
40
+
41
+ PREFIX_POSTFIX_NODES.each do |type|
42
+ define_method(:"visit_#{type}Node") do |o|
43
+ link_parents(o.operand, o)
44
+ end
45
+
46
+ end
47
+
48
+ CONDITIONAL_NODES.each do |type|
49
+ define_method(:"visit_#{type}Node") do |o|
50
+ [ link_parents(o.conditions, o),
51
+ link_parents(o.value, o),
52
+ o.else ? link_parents(o.else, o) : nil
53
+ ]
54
+ end
55
+ end
56
+ FUNC_CALL_NODES.each do |type|
57
+ define_method(:"visit_#{type}Node") do |o|
58
+ [link_parents(o.value, o), link_parents(o.arguments, o)]
59
+ end
60
+ end
61
+ FUNC_DECL_NODES.each do |type|
62
+ define_method(:"visit_#{type}Node") do |o|
63
+ [
64
+ o.value ? o.value : nil,
65
+ o.arguments.map { |x| link_parents(x, o) },
66
+ link_parents(o.function_body, o)
67
+ ]
68
+ end
69
+ end
70
+
71
+ def visit_ForNode(o)
72
+ [
73
+ o.init ? link_parents(o.init, o) : nil,
74
+ o.test ? link_parents(o.test, o) : nil,
75
+ o.counter ? link_parents(o.counter, o) : nil,
76
+ link_parents(o.value, o)
77
+ ]
78
+ end
79
+
80
+ def visit_ForInNode(o)
81
+ [
82
+ link_parents(o.left, o),
83
+ link_parents(o.right, o),
84
+ link_parents(o.value, o)
85
+ ]
86
+ end
87
+
88
+ def visit_TryNode(o)
89
+ [
90
+ link_parents(o.value, o),
91
+ o.catch_var ? o.catch_var : nil,
92
+ o.catch_block ? link_parents(o.catch_block, o) : nil,
93
+ o.finally_block ? link_parents(o.finally_block, o) : nil
94
+ ]
95
+ end
96
+
97
+ def visit_BracketAccessorNode(o)
98
+ [
99
+ link_parents(o.value, o),
100
+ link_parents(o.accessor, o)
101
+ ]
102
+ end
103
+
104
+ def visit_DotAccessorNode(o)
105
+ link_parents(o.value, o)
106
+ end
107
+
108
+ private
109
+ def link_parents node, parent
110
+ node.parent = parent
111
+
112
+ # call the visit method to traverse potential subnodes depends on the node class
113
+ # error if the node is not in the predefined list
114
+ #
115
+ raise "No visit method for '#{node.class}'" unless self.respond_to?("visit_#{node.class.name.split(/::/)[-1]}")
116
+ self.send(:"visit_#{node.class.name.split(/::/)[-1]}", node)
117
+ end
118
+ end
119
+ end
120
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsinstrument
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mushan Yang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rkelly
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Instrument JavaScript source code to monitor code coverage in pure Ruby
63
+ email:
64
+ - mushan.yang@hulu.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - jsinstrument.gemspec
75
+ - lib/jsinstrument.rb
76
+ - lib/jsinstrument/inserting.js
77
+ - lib/jsinstrument/instrumenter.rb
78
+ - lib/jsinstrument/version.rb
79
+ - lib/rkelly/node.rb
80
+ - lib/rkelly/parent_builder.rb
81
+ homepage: ''
82
+ licenses:
83
+ - MIT
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 1.8.25
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Instrument JavaScript source code to monitor code coverage in pure Ruby
106
+ test_files: []