Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

"""Coverage controllers for use by pytest-cov and nose-cov.""" 

 

import os 

import random 

import socket 

import sys 

 

import coverage 

from coverage.data import CoverageData 

 

from .compat import StringIO 

 

 

class CovController(object): 

"""Base class for different plugin implementations.""" 

 

def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None): 

"""Get some common config used by multiple derived classes.""" 

self.cov_source = cov_source 

self.cov_report = cov_report 

self.cov_config = cov_config 

self.cov_append = cov_append 

self.cov_branch = cov_branch 

self.config = config 

self.nodeid = nodeid 

 

self.cov = None 

self.combining_cov = None 

self.data_file = None 

self.node_descs = set() 

self.failed_slaves = [] 

self.topdir = os.getcwd() 

 

def pause(self): 

self.cov.stop() 

self.unset_env() 

 

def resume(self): 

self.cov.start() 

self.set_env() 

 

def set_env(self): 

"""Put info about coverage into the env so that subprocesses can activate coverage.""" 

if self.cov_source is None: 

os.environ['COV_CORE_SOURCE'] = os.pathsep 

else: 

os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source) 

config_file = os.path.abspath(self.cov_config) 

if os.path.exists(config_file): 

os.environ['COV_CORE_CONFIG'] = config_file 

else: 

os.environ['COV_CORE_CONFIG'] = os.pathsep 

os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) 

if self.cov_branch: 

os.environ['COV_CORE_BRANCH'] = 'enabled' 

 

@staticmethod 

def unset_env(): 

"""Remove coverage info from env.""" 

os.environ.pop('COV_CORE_SOURCE', None) 

os.environ.pop('COV_CORE_CONFIG', None) 

os.environ.pop('COV_CORE_DATAFILE', None) 

os.environ.pop('COV_CORE_BRANCH', None) 

 

@staticmethod 

def get_node_desc(platform, version_info): 

"""Return a description of this node.""" 

 

return 'platform %s, python %s' % (platform, '%s.%s.%s-%s-%s' % version_info[:5]) 

 

@staticmethod 

def sep(stream, s, txt): 

if hasattr(stream, 'sep'): 

stream.sep(s, txt) 

else: 

sep_total = max((70 - 2 - len(txt)), 2) 

sep_len = sep_total // 2 

sep_extra = sep_total % 2 

out = '%s %s %s\n' % (s * sep_len, txt, s * (sep_len + sep_extra)) 

stream.write(out) 

 

def summary(self, stream): 

"""Produce coverage reports.""" 

total = 0 

 

if not self.cov_report: 

with open(os.devnull, 'w') as null: 

total = self.cov.report(show_missing=True, ignore_errors=True, file=null) 

return total 

 

# Output coverage section header. 

if len(self.node_descs) == 1: 

self.sep(stream, '-', 'coverage: %s' % ''.join(self.node_descs)) 

else: 

self.sep(stream, '-', 'coverage') 

for node_desc in sorted(self.node_descs): 

self.sep(stream, ' ', '%s' % node_desc) 

 

# Produce terminal report if wanted. 

if any(x in self.cov_report for x in ['term', 'term-missing']): 

options = { 

'show_missing': ('term-missing' in self.cov_report) or None, 

'ignore_errors': True, 

'file': stream, 

} 

skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() 

if hasattr(coverage, 'version_info') and coverage.version_info[0] >= 4: 

options.update({'skip_covered': skip_covered or None}) 

total = self.cov.report(**options) 

 

# Produce annotated source code report if wanted. 

if 'annotate' in self.cov_report: 

annotate_dir = self.cov_report['annotate'] 

self.cov.annotate(ignore_errors=True, directory=annotate_dir) 

# We need to call Coverage.report here, just to get the total 

# Coverage.annotate don't return any total and we need it for --cov-fail-under. 

total = self.cov.report(ignore_errors=True, file=StringIO()) 

if annotate_dir: 

stream.write('Coverage annotated source written to dir %s\n' % annotate_dir) 

else: 

stream.write('Coverage annotated source written next to source\n') 

 

# Produce html report if wanted. 

if 'html' in self.cov_report: 

total = self.cov.html_report(ignore_errors=True, directory=self.cov_report['html']) 

stream.write('Coverage HTML written to dir %s\n' % self.cov.config.html_dir) 

 

# Produce xml report if wanted. 

if 'xml' in self.cov_report: 

total = self.cov.xml_report(ignore_errors=True, outfile=self.cov_report['xml']) 

stream.write('Coverage XML written to file %s\n' % self.cov.config.xml_output) 

 

# Report on any failed slaves. 

if self.failed_slaves: 

self.sep(stream, '-', 'coverage: failed slaves') 

stream.write('The following slaves failed to return coverage data, ' 

'ensure that pytest-cov is installed on these slaves.\n') 

for node in self.failed_slaves: 

stream.write('%s\n' % node.gateway.id) 

 

return total 

 

 

class Central(CovController): 

"""Implementation for centralised operation.""" 

 

def start(self): 

"""Erase any previous coverage data and start coverage.""" 

self.cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

config_file=self.cov_config) 

self.combining_cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

data_file=os.path.abspath(self.cov.config.data_file), 

config_file=self.cov_config) 

if self.cov_append: 

self.cov.load() 

else: 

self.cov.erase() 

self.cov.start() 

self.set_env() 

 

def finish(self): 

"""Stop coverage, save data to file and set the list of coverage objects to report on.""" 

 

self.unset_env() 

self.cov.stop() 

self.cov.save() 

 

self.cov = self.combining_cov 

self.cov.load() 

self.cov.combine() 

self.cov.save() 

 

node_desc = self.get_node_desc(sys.platform, sys.version_info) 

self.node_descs.add(node_desc) 

 

 

class DistMaster(CovController): 

"""Implementation for distributed master.""" 

 

def start(self): 

"""Ensure coverage rc file rsynced if appropriate.""" 

 

if self.cov_config and os.path.exists(self.cov_config): 

self.config.option.rsyncdir.append(self.cov_config) 

 

self.cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

config_file=self.cov_config) 

self.combining_cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

data_file=os.path.abspath(self.cov.config.data_file), 

config_file=self.cov_config) 

if self.cov_append: 

self.cov.load() 

else: 

self.cov.erase() 

self.cov.start() 

self.cov.config.paths['source'] = [self.topdir] 

 

def configure_node(self, node): 

"""Slaves need to know if they are collocated and what files have moved.""" 

 

node.slaveinput['cov_master_host'] = socket.gethostname() 

node.slaveinput['cov_master_topdir'] = self.topdir 

node.slaveinput['cov_master_rsync_roots'] = [str(root) for root in node.nodemanager.roots] 

 

def testnodedown(self, node, error): 

"""Collect data file name from slave.""" 

 

# If slave doesn't return any data then it is likely that this 

# plugin didn't get activated on the slave side. 

if not (hasattr(node, 'slaveoutput') and 'cov_slave_node_id' in node.slaveoutput): 

self.failed_slaves.append(node) 

return 

 

# If slave is not collocated then we must save the data file 

# that it returns to us. 

if 'cov_slave_data' in node.slaveoutput: 

data_suffix = '%s.%s.%06d.%s' % ( 

socket.gethostname(), os.getpid(), 

random.randint(0, 999999), 

node.slaveoutput['cov_slave_node_id'] 

) 

 

cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

data_suffix=data_suffix, 

config_file=self.cov_config) 

cov.start() 

data = CoverageData() 

data.read_fileobj(StringIO(node.slaveoutput['cov_slave_data'])) 

cov.data.update(data) 

cov.stop() 

cov.save() 

path = node.slaveoutput['cov_slave_path'] 

self.cov.config.paths['source'].append(path) 

 

# Record the slave types that contribute to the data file. 

rinfo = node.gateway._rinfo() 

node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info) 

self.node_descs.add(node_desc) 

 

def finish(self): 

"""Combines coverage data and sets the list of coverage objects to report on.""" 

 

# Combine all the suffix files into the data file. 

self.cov.stop() 

self.cov.save() 

self.cov = self.combining_cov 

self.cov.load() 

self.cov.combine() 

self.cov.save() 

 

 

class DistSlave(CovController): 

"""Implementation for distributed slaves.""" 

 

def start(self): 

"""Determine what data file and suffix to contribute to and start coverage.""" 

 

# Determine whether we are collocated with master. 

self.is_collocated = (socket.gethostname() == self.config.slaveinput['cov_master_host'] and 

self.topdir == self.config.slaveinput['cov_master_topdir']) 

 

# If we are not collocated then rewrite master paths to slave paths. 

if not self.is_collocated: 

master_topdir = self.config.slaveinput['cov_master_topdir'] 

slave_topdir = self.topdir 

if self.cov_source is not None: 

self.cov_source = [source.replace(master_topdir, slave_topdir) 

for source in self.cov_source] 

self.cov_config = self.cov_config.replace(master_topdir, slave_topdir) 

 

# Erase any previous data and start coverage. 

self.cov = coverage.Coverage(source=self.cov_source, 

branch=self.cov_branch, 

data_suffix=True, 

config_file=self.cov_config) 

if self.cov_append: 

self.cov.load() 

else: 

self.cov.erase() 

self.cov.start() 

self.set_env() 

 

def finish(self): 

"""Stop coverage and send relevant info back to the master.""" 

self.unset_env() 

self.cov.stop() 

 

if self.is_collocated: 

# We don't combine data if we're collocated - we can get 

# race conditions in the .combine() call (it's not atomic) 

# The data is going to be combined in the master. 

self.cov.save() 

 

# If we are collocated then just inform the master of our 

# data file to indicate that we have finished. 

self.config.slaveoutput['cov_slave_node_id'] = self.nodeid 

else: 

self.cov.combine() 

self.cov.save() 

# If we are not collocated then add the current path 

# and coverage data to the output so we can combine 

# it on the master node. 

 

# Send all the data to the master over the channel. 

self.config.slaveoutput['cov_slave_path'] = self.topdir 

self.config.slaveoutput['cov_slave_node_id'] = self.nodeid 

buff = StringIO() 

self.cov.data.write_fileobj(buff) 

self.config.slaveoutput['cov_slave_data'] = buff.getvalue() 

 

def summary(self, stream): 

"""Only the master reports so do nothing.""" 

 

pass