Added new features to buildbot.
[apps/native/sample/sample-core-components.git] / tool / development / buildbot / nativesamples.py
1 # -*- coding: utf-8 -*-
2 import sys, os
3 import sqlite3
4 import ConfigParser
5 import subprocess
6 import re
7 import json
8 from pygerrit.ssh import GerritSSHClient
9 import smtplib
10 import base64
11 import traceback
12 import datetime
13 import tempfile
14 import xml.etree.ElementTree as ET
15 import shutil
16 import pyparsing
17
18 # Import the email modules we'll need
19 from email.mime.text import MIMEText
20 from email.mime.multipart import MIMEMultipart
21
22 reload(sys)
23 sys.setdefaultencoding('utf-8')
24
25 def numberOfProcesses(pgrepArg):
26         numberOfPollingProcesses = "0"
27         try:
28                 numberOfPollingProcesses = subprocess.check_output(["pgrep", "-c", "-f", pgrepArg])
29         except subprocess.CalledProcessError as err:
30                 numberOfPollingProcesses = err.output
31         return int(numberOfPollingProcesses)
32
33
34 class NativeSamplesConfig:
35         configFilePath = None
36         gerritServerName = None
37         gerritUser = None
38         gerritPort = None
39         gerritSshPrefixUrl = None
40         tizenSdkPath = None
41         sampleCoreComponentsPath = None
42         tizenBinary = None
43         tccBinary = None
44         checkpatchTizenBinary = None
45         convertToSampleProjectBinary = None
46         nativeSampleProjectList = None
47
48         emailNotificationsSmtpServerName = None
49         emailNotificationsSmtpPort = None
50         emailNotificationsSmtpUser = None
51         emailNotificationsSmtpB64EncodedPassword = None
52         emailNotificationsMailFrom = None
53         emailNotificationsMailTo = None
54         emailNotificationsMailCc = None
55         emailNotificationsMailBcc = None
56
57         def __init__(self, configFilePath = None):
58                 if configFilePath is not None:
59                         self.readConfigFile(configFilePath)
60         def readConfigFile(self, configFilePath):
61                 if os.path.exists(configFilePath):
62                         self.configFilePath = configFilePath
63                         config = ConfigParser.SafeConfigParser({'GerritServerName': None,
64                                                                                                         'GerritUser': None,
65                                                                                                         'GerritPort': None,
66                                                                                                         'TizenSdkPath': None,
67                                                                                                         'SampleCoreComponentsPath': None,
68                                                                                                         'SmtpServerName': None,
69                                                                                                         'SmtpPort': None,
70                                                                                                         'SmtpUser': None,
71                                                                                                         'SmtpB64EncodedPassword': None,
72                                                                                                         'MailFrom': None,
73                                                                                                         'MailTo': None,
74                                                                                                         'MailCc': None,
75                                                                                                         'MailBcc': None})
76                         config.read(configFilePath)
77                         self.gerritServerName =  config.get('Gerrit', 'GerritServerName')
78                         self.gerritUser = config.get('Gerrit', 'GerritUser')
79                         self.gerritPort = config.get('Gerrit', 'GerritPort')
80                         self.tizenSdkPath = config.get('BuildTools', 'TizenSdkPath')
81                         self.sampleCoreComponentsPath =  config.get('BuildTools', 'SampleCoreComponentsPath')
82                         #ssh://[username@]servername[:port]/
83                         self.gerritSshPrefixUrl = "ssh://"
84                         if self.gerritUser is not None:
85                                 self.gerritSshPrefixUrl += self.gerritUser + "@"
86                         self.gerritSshPrefixUrl += self.gerritServerName
87                         if self.gerritPort is not None:
88                                 self.gerritSshPrefixUrl += ":" + self.gerritPort
89                         self.gerritSshPrefixUrl += "/"
90                         self.nativeSampleProjectList = config.get('Gerrit', 'RepositoryList').split(",")
91                         for i, prj in enumerate(self.nativeSampleProjectList):
92                                 self.nativeSampleProjectList[i] = prj.strip("\n\t")
93                         if os.path.exists(self.tizenSdkPath):
94                                 self.tizenBinary = os.path.join(self.tizenSdkPath, "tools/ide/bin/tizen")
95                         if os.path.exists(self.sampleCoreComponentsPath):
96                                 self.tccBinary = os.path.join(self.sampleCoreComponentsPath, "tool/tcc.py")
97                                 self.convertToSampleProjectBinary = os.path.join(self.sampleCoreComponentsPath, "tool/development/convert-tpk-to-sample-project.sh")
98                                 self.checkpatchTizenBinary = os.path.join(self.sampleCoreComponentsPath, "tool/checkpatch_tizen.pl")
99                         self.emailNotificationsSmtpServerName = config.get('EmailNotifications', 'SmtpServerName')
100                         self.emailNotificationsSmtpPort = int(config.get('EmailNotifications', 'SmtpPort'))
101                         self.emailNotificationsSmtpUser = config.get('EmailNotifications', 'SmtpUser')
102                         self.emailNotificationsSmtpB64EncodedPassword = config.get('EmailNotifications', 'SmtpB64EncodedPassword')
103                         self.emailNotificationsMailFrom = config.get('EmailNotifications', 'MailFrom')
104                         self.emailNotificationsMailTo = config.get('EmailNotifications', 'MailTo').split(",")
105                         self.emailNotificationsMailCc = config.get('EmailNotifications', 'MailCc').split(",")
106                         self.emailNotificationsMailBcc = config.get('EmailNotifications', 'MailBcc').split(",")
107                 else:
108                         raise ValueError('Config file name:' + configFilePath + " does not exist")
109
110 class NativeSample:
111         projectName = None
112         projectPath = None
113         repositoryUrl = None
114         def __init__(self, projectName, projectPath, repositoryUrl):
115                 self.projectName = projectName
116                 self.projectPath = projectPath
117                 self.repositoryUrl = repositoryUrl
118         def __repr__(self):
119                 return  ('NativeSample(projectName: %s, ' \
120                                  'projectPath: %s, '\
121                                  'repositoryUrl = %s)') % (
122                                 self.projectName,
123                                 self.projectPath,
124                                 self.repositoryUrl)
125
126 class NativeSampleChange:
127         revisionId = None
128         changeId = None
129         nativeSample = None
130         def __init__(self, nativeSample, revId, changeId = None):
131                 self.nativeSample = nativeSample
132                 self.revisionId = revId
133                 self.changeId = changeId
134         def __repr__(self):
135                 return  ('NativeSampleChange(nativeSample: %s, ' \
136                                  'revisionId: %s, '\
137                                  'changeId = %s)') % (
138                                 self.nativeSample,
139                                 self.revisionId,
140                                 self.changeId)
141
142 class NativeSamplesDatabase:
143         createDatabaseScript = """
144         CREATE TABLE IF NOT EXISTS changes_to_evaluate (
145                 id INTEGER NOT NULL PRIMARY KEY,
146                 project_name TEXT NOT NULL,
147                 revision_id TEXT NOT NULL
148         );
149         """
150         insertChangeStatement = """
151         INSERT INTO changes_to_evaluate(project_name, revision_id) VALUES(:project_name, :revision_id)
152         """
153         conn = None
154         databaseFile = None
155         def __init__(self, dbfile):
156                 self.databaseFile = dbfile
157                 self.conn = sqlite3.connect(dbfile, isolation_level = None)
158                 self.conn.execute("PRAGMA busy_timeout = 30000")
159                 self.cur = self.conn.cursor()
160                 self.cur.executescript(self.createDatabaseScript)
161                 self.conn.commit()
162         def saveNativeSampleChange(self, nativeSampleChange):
163                 self.cur.execute(self.insertChangeStatement, {'project_name':nativeSampleChange.nativeSample.projectName, 'revision_id': nativeSampleChange.revisionId})
164         def saveNativeSampleChange(self, nativeSampleChange):
165                 self.cur.execute(self.insertChangeStatement, {'project_name':nativeSampleChange.nativeSample.projectName, 'revision_id': nativeSampleChange.revisionId})
166         def deleteNativeSampleChange(self, nativeSampleChange):
167                 self.cur.execute("DELETE FROM changes_to_evaluate WHERE project_name = :project_name AND revision_id = :revision_id", {'project_name':nativeSampleChange.nativeSample.projectName, 'revision_id': nativeSampleChange.revisionId})
168         def getPendingNativeSampleChanges(self, nativeSamples):
169                 ret = []
170                 for row in self.cur.execute("SELECT id, project_name, revision_id FROM changes_to_evaluate ORDER BY id ASC"):
171                         ret.append(NativeSampleChange(NativeSample(row[1], None, None), row[2]))
172                 return ret
173
174 class NativeSampleGerritManager:
175         client = None
176         version = None
177         def __init__(self, config):
178                 self.client = GerritSSHClient(config.gerritServerName, username=config.gerritUser, port = int(config.gerritPort), keepalive=True)
179         def getVersion(self):
180                 return self.client.run_gerrit_command("version").stdout.read()
181         def getChangeInfo(self, nativeSampleChange):
182                 if nativeSampleChange.revisionId is None:
183                         raise ValueError('Native sample revision Id cannot be None for: ' + str(nativeSampleChange))
184                 jsonChangeIdStr = self.client.run_gerrit_command("query --format=JSON --current-patch-set=" + nativeSampleChange.revisionId +" project:" + nativeSampleChange.nativeSample.projectName + " limit:1").stdout.read()
185                 changeInfo = json.loads(jsonChangeIdStr.split("\n")[0])
186                 if 'id' in changeInfo.keys() and 'currentPatchSet' in changeInfo.keys():
187                         return changeInfo
188                 else:
189                         return None
190         def addCommentToChange(self, nativeSampleChange, commentText):
191                 command = subprocess.list2cmdline(["review", "-m", commentText[:20000].replace("\n", "\n "), nativeSampleChange.revisionId])
192                 result = self.client.run_gerrit_command(command)
193                 print result.stdout.read(), result.stderr.read()
194
195 class EmailSender:
196         smtpServer = None
197         smtpPort = None
198         smtpUser = None
199         smtpPassword = None
200         mailFrom = None
201         mailTo = None
202         mailCc = None
203         mailBcc = None
204         def __init__(self, config):
205                 self.smtpServer = config.emailNotificationsSmtpServerName
206                 self.smtpPort = config.emailNotificationsSmtpPort
207                 self.smtpUser = config.emailNotificationsSmtpUser
208                 self.smtpPassword = config.emailNotificationsSmtpB64EncodedPassword
209                 self.mailFrom = config.emailNotificationsMailFrom
210                 self.mailTo = config.emailNotificationsMailTo
211                 self.mailCc = config.emailNotificationsMailCc
212                 self.mailBcc = config.emailNotificationsMailBcc
213         def send(self, mailSubject, mailText = None, mailTo = None, mailFrom = None, mailCc = None, mailBcc = None, mimeType = None):
214                 msg = MIMEText(mailText, mimeType or 'plain', 'utf-8')
215                 msg['Subject'] = mailSubject
216                 msg['From'] = self.mailFrom
217                 msg['To'] = ",".join(mailTo or self.mailTo)
218                 msg['CC'] = ",".join(mailCc or self.mailCc)
219                 msg['BCC'] = ",".join(mailBcc or self.mailBcc)
220
221                 s = smtplib.SMTP(host = self.smtpServer, port = self.smtpPort)
222
223                 s.login(self.smtpUser, base64.b64decode(self.smtpPassword))
224                 s.sendmail(msg['From'], msg['To'].split(","), msg.as_string())
225                 s.quit()
226
227 class NativeSamples:
228         #enum like source type
229         SourceTypeSampleProject = 0
230         SourceTypeTpkProject = 1
231         SampleTemplateTypeMobile = 0
232         SampleTemplateTypeWearable = 1
233         SampleTemplateTypeWatchface = 2
234         SampleTemplateTypeService = 3
235         samplesList = []
236         config = None
237         nativeSamplesRootPath = None
238         databaseModel = None
239         gerrit = None
240         emailSender = None
241         def __init__(self, rootBuildPath):
242                 self.nativeSamplesRootPath = rootBuildPath
243                 self.databaseModel = NativeSamplesDatabase(os.path.join(rootBuildPath, ".native-samples.db"))
244                 self.config = NativeSamplesConfig(os.path.join(rootBuildPath, ".native-samples.cfg"))
245                 self.samplesList = self.config.nativeSampleProjectList
246                 self.gerrit = NativeSampleGerritManager(self.config)
247                 self.emailSender = EmailSender(self.config)
248         def _cloneSampleFromGerrit(self, nativeSample):
249                 if os.path.exists(nativeSample.projectPath):
250                         raise ValueError('Path ' + projectPath + ' of project ' + projectName + ' clone already exist')
251                 print "Cloning repository of ", nativeSample
252                 subprocess.check_call(["git", "clone", nativeSample.repositoryUrl, nativeSample.projectPath])
253         def _getNativeSample(self, projectName):
254                 return NativeSample(projectName, os.path.join(self.nativeSamplesRootPath, projectName), self.config.gerritSshPrefixUrl + projectName)
255
256         def pollForChanges(self, projectList = None):
257                 if projectList is None:
258                         projectList = self.samplesList
259                 for nativeSampleProjectName in projectList:
260                         nativeSample = self._getNativeSample(nativeSampleProjectName)
261                         if not os.path.exists(nativeSample.projectPath):
262                                 self._cloneSampleFromGerrit(nativeSample)
263                         gitCommandList = ["git", "--git-dir=" + os.path.join(nativeSample.projectPath, ".git")]
264                         fetchOutput = ""
265                         try:
266                                 fetchOutput = subprocess.check_output(gitCommandList + ["fetch", "origin", "refs/changes/*:refs/remotes/origin/gerrit/*"], stderr=subprocess.STDOUT)
267                         except subprocess.CalledProcessError as error:
268                                 if error.returncode == 128:
269                                         print 'network or server error - trying to fetch next project'
270                                 else:
271                                         raise
272                         if len(fetchOutput) > 0:
273                                 print fetchOutput
274                         changeIds = re.findall("(?<=-> ).*", fetchOutput)
275                         if changeIds is None:
276                                 continue
277                         for changeId in changeIds:
278                                 revisionId = subprocess.check_output(gitCommandList + [ "log", "--pretty=format:%H", "-1", changeId])
279                                 self.databaseModel.saveNativeSampleChange(NativeSampleChange(nativeSample, revisionId))
280         def _cleanGitRepo(self, repoDir):
281                 curDir = os.getcwd()
282                 try:
283                         os.chdir(repoDir)
284                         subprocess.call(["git", "checkout", "-q", "."], stderr=subprocess.STDOUT)
285                         subprocess.check_call(["git", "clean", "-fdxq", "."], stderr=subprocess.STDOUT)
286                 finally:
287                         os.chdir(curDir)
288         def _cleanCurrentGitRepo(self):
289                 self._cleanGitRepo(os.getcwd())
290         def _cleanRepoAndCheckoutToRevision(self, sampleChange = None, repoPath = None, revision = None):
291                 curDir = os.getcwd()
292                 try:
293                         os.chdir(repoPath or sampleChange.nativeSample.projectPath or curDir)
294                         self._cleanCurrentGitRepo()
295                         subprocess.check_call(["git", "checkout", "-qf", revision or sampleChange.revisionId])
296                 finally:
297                         os.chdir(curDir)
298         def _sourceType(self, sampleDirectoryPath = None):
299                 curDir = os.getcwd()
300                 try:
301                         if sampleDirectoryPath is not None:
302                                 os.chdir(sampleDirectoryPath)
303
304                         #if sample contains sample.xml then we assume that it is sample project type
305                         if os.path.exists("sample.xml"):
306                                 return self.SourceTypeSampleProject
307                         if os.path.exists("tizen-manifest.xml"):
308                                 return self.SourceTypeTpkProject
309                         raise ValueError("Can't determine source project type in path:" + os.getcwd())
310                 finally:
311                         os.chdir(curDir)
312         def _currentSourceType(self):
313                 return self._sourceType(os.getcwd())
314         def _directoryContainsFileWithString(self, directory, stringToFind):
315                 try:
316                         subprocess.check_call(["grep", "-qr", stringToFind, directory])
317                         return True
318                 except subprocess.CalledProcessError:
319                         return False
320
321         def _sampleTemplateType(self, sampleDirectoryPath = None):
322                 if sampleDirectoryPath is None:
323                         sampleDirectoryPath = os.getcwd()
324                 ret = None
325                 #if it is service type then profile doesn't matter
326                 if self._directoryContainsFileWithString(sampleDirectoryPath, "service_app_lifecycle_callback_s"):
327                         ret = self.SampleTemplateTypeService
328                 elif self._directoryContainsFileWithString(sampleDirectoryPath, "watch_app_lifecycle_callback_s"):
329                         ret = self.SampleTemplateTypeWatchface
330                 if ret is None:
331                         sourceType = self._sourceType()
332                         tizenXmlFilePath = None
333                         descriptionXmlFilePath = None
334                         if sourceType == self.SourceTypeSampleProject:
335                                 descriptionXmlFilePath = os.path.join(os.getcwd(), "description.xml")
336                         elif sourceType == self.SourceTypeTpkProject:
337                                 if not os.path.exists(os.path.join(os.getcwd(), "sample-project-src/description.xml")):
338                                         tizenXmlFilePath = os.path.join(os.getcwd(), "tizen-manifest.xml")
339                                 else:
340                                         descriptionXmlFilePath = os.path.join(os.getcwd(), "sample-project-src/description.xml")
341                         profileType = "undefined"
342                         if descriptionXmlFilePath is not None:
343                                 if not os.path.exists(descriptionXmlFilePath):
344                                         raise ValueError("Can't find description.xml file in %s. Can't determine sample template type." % descriptionXmlFilePath)
345                                 xmlTree = ET.parse(descriptionXmlFilePath)
346                                 xmlRoot = xmlTree.getroot()
347                                 xmlnsPrefix = xmlRoot.tag.replace("Overview", "")
348                                 profileType = xmlRoot.iter(xmlnsPrefix + 'ProfileName').next().text.lower()
349                         if tizenXmlFilePath is not None:
350                                 if not os.path.exists(tizenXmlFilePath):
351                                         raise ValueError("Can't find tizen-manifest.xml file in %s. Can't determine sample template type." % tizenXmlFilePath)
352                                 xmlTree = ET.parse(tizenXmlFilePath)
353                                 xmlRoot = xmlTree.getroot()
354                                 xmlnsPrefix = xmlRoot.tag.replace("manifest", "")
355                                 profileType = xmlRoot.find(xmlnsPrefix + 'profile').attrib["name"]
356
357                         if profileType == "wearable":
358                                 ret = self.SampleTemplateTypeWearable
359                         elif profileType == "mobile":
360                                 ret = self.SampleTemplateTypeMobile
361                 if ret is None:
362                         raise ValueError("Can't determine sample template type")
363                 return ret
364
365         def buildTpk(self):
366                 subprocess.check_call(["rm", "-rf", "Debug"], stderr=subprocess.STDOUT)
367                 try:
368                         output = subprocess.check_output([self.config.tizenBinary, "build-native", "-a", "arm"], stderr=subprocess.STDOUT)
369                         #sometimes build-native returns 0 exit status if linker error occured
370                         return not "clang++: error" in output, output
371                 except subprocess.CalledProcessError as error:
372                         return False, traceback.format_exc() + "\n" + error.output
373
374         def buildSampleFromTpkBranch(self, nativeSampleChange, changeInfo = None):
375                 print "========> TPK_BUILD for ", nativeSampleChange.nativeSample.projectName, " and changeId:", nativeSampleChange.changeId
376                 curDir = os.getcwd()
377                 try:
378                         if changeInfo is None:
379                                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
380                         os.chdir(nativeSampleChange.nativeSample.projectPath)
381                         if os.path.exists(os.path.join(nativeSampleChange.nativeSample.projectPath, "Build")):
382                                 result, output = self.buildTpk()
383                                 if result:
384                                         print output
385                                         print "SUCCESS: built change " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
386                                 else:
387                                         print "ERROR:", output
388                                         print "FAIL: built change " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
389                                         return False, output
390                         else:
391                                 print 'No Tizen CLI configuration in ' + nativeSampleChange.nativeSample.projectPath
392                 finally:
393                         os.chdir(curDir)
394                 return True, ""
395         def buildTpkFromProject(self, nativeSample, uniquePostfix):
396                 curDir = os.getcwd()
397                 tmpProjectPath = None
398                 destSamplesPathDir = None
399                 try:
400                         os.chdir(nativeSample.projectPath)
401                         projectProfileWithVersion = None
402                         projectDefFile = os.path.join(nativeSample.projectPath,'project/project_def.prop')
403                         if not os.path.exists(projectDefFile):
404                                 return False, "No Tizen CLI project file:" + projectDefFile
405                         f = open(projectDefFile)
406                         for line in f:
407                                 res = re.search("\s*profile\s*=\s*", line)
408                                 if res is not None:
409                                         projectProfileWithVersion = re.sub("\s*profile\s*=\s*", "", line).strip("\n\t ")
410                                         break
411                         tizenVersion = "tizen" + re.search("-[0-9].[0-9](.[0-9])?", projectProfileWithVersion).group(0)
412                         profileName = re.search(".*(?=-)", projectProfileWithVersion).group(0)
413                         tmpProjectPath = tempfile.mkdtemp()
414                         tmpProjectName = "SampleProjectToBuild"
415                         projectBasename = os.path.basename(nativeSample.projectPath)
416                         samplesUniqueProjectName = projectBasename + uniquePostfix
417                         projectPrefixInSamplesDir = "buildbot"
418                         destSamplesPathDir = os.path.join(self.config.tizenSdkPath, "platforms", tizenVersion, profileName, "samples/Template/Native", projectPrefixInSamplesDir)
419                         if not os.path.exists(destSamplesPathDir):
420                                 os.makedirs(destSamplesPathDir)
421                         destSamplesPathDir = os.path.join(destSamplesPathDir, samplesUniqueProjectName)
422                         print "Copying sample dir to " + destSamplesPathDir
423                         subprocess.check_call(["cp", "-r", nativeSample.projectPath, destSamplesPathDir])
424                         print "removing " + destSamplesPathDir + "/.git"
425                         shutil.rmtree(os.path.join(destSamplesPathDir, ".git"), ignore_errors=True)
426                         createProjectArgs = [self.config.tizenBinary, "create", "native-project", "-n", tmpProjectName, "-p", projectProfileWithVersion, "-t", samplesUniqueProjectName, "--", tmpProjectPath]
427                         print "Creating project from sample template: " + " ".join(createProjectArgs)
428                         subprocess.check_call(createProjectArgs)
429                         os.chdir(os.path.join(tmpProjectPath, tmpProjectName))
430                         print "building project using command line"
431                         return self.buildTpk()
432                 except subprocess.CalledProcessError as error:
433                         return False, traceback.format_exc() + "\n" + error.output
434                 finally:
435                         if tmpProjectPath is not None:
436                                 shutil.rmtree(tmpProjectPath, ignore_errors=True)
437                         if destSamplesPathDir is not None:
438                                 shutil.rmtree(destSamplesPathDir, ignore_errors=True)
439                         os.chdir(curDir)
440         def buildSampleFromProjectBranch(self, nativeSampleChange, changeInfo = None):
441                 print "========> SAMPLE_PROJECT_BUILD for ", nativeSampleChange.nativeSample.projectName, " and changeId:", nativeSampleChange.changeId
442                 curDir = os.getcwd()
443                 try:
444                         tmpProjectPath = None
445                         destSamplesPathDir = None
446                         if changeInfo is None:
447                                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
448                         if os.path.exists(os.path.join(nativeSampleChange.nativeSample.projectPath, "project/Build")):
449                                 print "building project using command line"
450                                 result, output = self.buildTpkFromProject(nativeSampleChange.nativeSample, nativeSampleChange.changeId)
451                                 if result:
452                                         print output
453                                         print "SUCCESS: built change " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
454                                 else:
455                                         print "ERROR:", output
456                                         print "FAIL: built change " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
457                                         return False, output
458                         else:
459                                 print 'No Tizen CLI configuration in ' + nativeSampleChange.nativeSample.projectPath
460                 finally:
461                         if tmpProjectPath is not None:
462                                 shutil.rmtree(tmpProjectPath, ignore_errors=True)
463                         if destSamplesPathDir is not None:
464                                 shutil.rmtree(destSamplesPathDir, ignore_errors=True)
465                         os.chdir(curDir)
466                 return True, ""
467         def invokeConvert(self, revision, outputDirectory = None):
468                 if outputDirectory is None:
469                         outputDirectory = os.getcwd()
470                 try:
471                         subprocess.check_call([self.config.convertToSampleProjectBinary, "-v", "-r", revision, "-o", outputDirectory], stderr=subprocess.STDOUT)
472                 except subprocess.CalledProcessError as error:
473                         return False, traceback.format_exc() + "\n" + error.output
474
475         def invokeTcc(self):
476                 templateType = self._sampleTemplateType()
477                 templatePostfix = "undefined"
478                 if templateType == self.SampleTemplateTypeMobile:
479                         templatePostfix = "mobile"
480                 elif templateType == self.SampleTemplateTypeWatchface:
481                         templatePostfix = "watchface"
482                 elif templateType == self.SampleTemplateTypeWearable:
483                         templatePostfix = "wearable"
484                 elif templateType == self.SampleTemplateTypeService:
485                         templatePostfix = "service"
486                 templatePath = os.path.join(self.config.sampleCoreComponentsPath, "rule/" + templatePostfix)
487                 try:
488                         tccOutput = subprocess.check_output([self.config.tccBinary, "-c", templatePath, "."], stderr=subprocess.STDOUT)
489                         result = re.search("[0-9]+(?= code sections are modified)", tccOutput)
490                         if int(result.group(0)) > 0:
491                                 return False, tccOutput
492                         result = re.search("[0-9]+(?= template files are removed)", tccOutput)
493                         if int(result.group(0)) > 0:
494                                 return False, tccOutput
495                 except subprocess.CalledProcessError as error:
496                         return False, traceback.format_exc() + "\n" + error.output
497
498                 return True, tccOutput
499
500
501         def checkSampleUsingTcc(self, nativeSampleChange, changeInfo = None):
502                 print "========> TCC for ", nativeSampleChange.nativeSample.projectName, " and changeId:", nativeSampleChange.changeId
503                 curDir = os.getcwd()
504                 try:
505                         if changeInfo is None:
506                                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
507
508                         os.chdir(nativeSampleChange.nativeSample.projectPath)
509                         self._cleanRepoAndCheckoutToRevision(sampleChange = nativeSampleChange)
510                         sourceType = self._currentSourceType()
511                         if sourceType == self.SourceTypeTpkProject:
512                                 tempTccOutputDir = tempfile.mkdtemp()
513                                 self.invokeConvert(revision = nativeSampleChange.revisionId, outputDirectory = tempTccOutputDir)
514                                 os.chdir(tempTccOutputDir)
515                         elif sourceType != self.SourceTypeSampleProject:
516                                 raise ValueError("Wrong source type to use tcc tool")
517                         result, tccOutput = self.invokeTcc()
518                         print tccOutput
519                         if not result:
520                                 print 'FAIL: tcc for ', nativeSampleChange.nativeSample.projectName, ' and changeId:', nativeSampleChange.changeId
521                                 return False, tccOutput
522                 finally:
523                         self._cleanRepoAndCheckoutToRevision(sampleChange = nativeSampleChange)
524                         os.chdir(curDir)
525                 print 'SUCCCESS: tcc for ', nativeSampleChange.nativeSample.projectName, ' and changeId:', nativeSampleChange.changeId
526                 return True, ""
527         def invokeCheckpatchTizen(self):
528                 try:
529                         filesToCheck = []
530                         for root, dirs, files in os.walk(os.getcwd()):
531                                 for fileName in files:
532                                         if fileName.endswith(".c") or fileName.endswith(".h"):
533                                                 filesToCheck.append(os.path.join(root, fileName))
534                         checkPatchTizenOutput = subprocess.check_output(["perl", self.config.checkpatchTizenBinary] + filesToCheck, stderr=subprocess.STDOUT)
535                         return True, checkPatchTizenOutput
536                 except subprocess.CalledProcessError as err:
537                         return False, traceback.format_exc() + "\noutput:" + err.output
538         def invokeCloc(self):
539                 try:
540                         sourceType = self._currentSourceType()
541                         additionalArgs = []
542                         if sourceType == self.SourceTypeTpkProject:
543                                 additionalArgs.append("--exclude-dir=Build")
544                         elif sourceType == self.SourceTypeSampleProject:
545                                 additionalArgs.append("--exclude-dir=project/Build")
546
547                         output = subprocess.check_output(["cloc", "--exclude-ext=py",  "--csv", "--quiet", "."] + additionalArgs)
548                         languagesOutputList = []
549                         for i, line in enumerate(output.split("\n")):
550                                 if i in [0, 1] or not line:
551                                         continue
552                                 #we ommit first column which is rownum column
553                                 languagesOutputList.append(line.split(","))
554                         return True, output, languagesOutputList
555                 except subprocess.CalledProcessError as err:
556                         return False, traceback.format_exc() + "\noutput:" + err.output
557         def invokeAspell(self, ignoredWordsList = None):
558                 class WordInfo:
559                         def __init__(self, wordStr = None, hintsStr = None, positionInLine = None, lineNumber = None, lineStr = None, wordInfo = None):
560                                 if wordInfo is None:
561                                         self.wordStr = wordStr
562                                         self.hintsStr = hintsStr
563                                         self.positionInLine  = positionInLine
564                                         self.lineNumber = lineNumber
565                                         self.lineStr = lineStr
566                                 else:
567                                         self.wordStr = wordInfo.wordStr
568                                         self.hintsStr = wordInfo.hintsStr
569                                         self.positionInLine = wordInfo.positionInLine
570                                         self.lineNumber = wordInfo.lineNumber
571                                         self.lineStr = wordInfo.lineStr
572                         def __eq__(self, other):
573                                 return (isinstance(other, self.__class__) and self.__dict__ == other.__dict__)
574
575                         def __ne__(self, other):
576                                 return not self.__eq__(other)
577                 def getDataFromAspellOutput(aspellOutput):
578                         outputLines = filter(None, aspellOutput.split("\n"))
579                         wordsInfo = []
580                         foundWords = []
581                         longestOutputLine = 0
582                         if outputLines is not None and len(outputLines) > 0:
583                                 quotedStringOrComment = pyparsing.cppStyleComment | pyparsing.QuotedString(quoteChar = '"', escChar='\\', multiline = True).ignore(pyparsing.cppStyleComment)
584                                 for outputLine in outputLines:
585                                         wordParts = re.search("[\w']+ [0-9]+ [0-9]+", outputLine).group(0).split(" ")
586                                         tmpWord = WordInfo(wordStr = wordParts[0], hintsStr = outputLine.split(":")[1], positionInLine = int(wordParts[2]))
587                                         wordsInfo.append(tmpWord)
588                                 longestOutputLine = len(max(wordsInfo, key=lambda wordInfo: len(wordInfo.wordStr)).wordStr)
589                                 for i in range(len(wordsInfo)):
590                                         wordInfoTmp = WordInfo(wordInfo = wordsInfo[i])
591                                         regExStr = "(?<=^.{"+str(wordInfoTmp.positionInLine)+"})"+"((\\b{0}\\b)|(\\b{0}_+)|(\\b{0}[0-9]+))".format(wordInfoTmp.wordStr)
592                                         wordRegexToken = pyparsing.Regex(regExStr, flags = re.MULTILINE)
593                                         for r, start, end in quotedStringOrComment.parseWithTabs().scanString(fileContent):
594                                                 for res, s, e in wordRegexToken.scanString(r[0]):
595                                                         newWord = WordInfo(wordInfo = wordInfoTmp)
596                                                         newWord.lineNumber = fileContent[:start +s].count("\n")
597                                                         newWord.lineStr = lines[newWord.lineNumber]
598                                                         if not any(x == newWord for x in foundWords):
599                                                                 foundWords.append(newWord)
600                                                         #let's sort by line numbers now
601                                 foundWords.sort(key=lambda wordInfo: wordInfo.lineNumber)
602                         return foundWords, longestOutputLine
603                 try:
604                         aspellSummary = ""
605                         aspellIgnoreDictPath = ""
606                         aspellIgnoreDictPathArg = ""
607                         sourceType = self._currentSourceType()
608                         dictTemporaryFile = None
609                         aspellDictionaryObject = None
610                         if sourceType == self.SourceTypeTpkProject:
611                                 aspellIgnoreDictPath = os.path.join(os.getcwd(), "sample-project-src/aspell.ignore.json")
612                         elif sourceType == self.SourceTypeSampleProject:
613                                 aspellIgnoreDictPath = os.path.join(os.getcwd(), "aspell.ignore.json")
614                         if os.path.exists(aspellIgnoreDictPath):
615                                 jsonFile = open(aspellIgnoreDictPath)
616                                 aspellDictionaryObject = json.load(jsonFile)
617                                 jsonFile.close()
618                                 dictTemporaryFile = tempfile.NamedTemporaryFile()
619                                 dictTemporaryFile.write("personal_ws-1.1 en 0\n")
620                                 for dictItem in aspellDictionaryObject:
621                                         dictTemporaryFile.write(dictItem["word"] +"\n")
622                                 dictTemporaryFile.flush()
623                                 aspellIgnoreDictPathArg = " --personal=" + dictTemporaryFile.name + " "
624                         for root, dirs, files in os.walk(os.getcwd()):
625                                 for fileName in files:
626                                         if fileName.endswith(".c") or fileName.endswith(".h"):
627                                                 fullFilePath = os.path.join(root, fileName)
628                                                 relativeFilePath = os.path.relpath(fullFilePath)
629                                                 file = open(fullFilePath)
630                                                 lines = file.readlines()
631                                                 fileContent = "".join(lines)
632                                                 file.close()
633
634                                                 for i, line in enumerate(lines):
635                                                         if line.startswith("*"):
636                                                                 return False, "Please fix line number:" + str(i+1) + " in file:" + relativeFilePath + ". It shouldn't start with '*' because it breaks aspell"
637                                                 aspell = subprocess.Popen("bash -c 'set -o pipefail; aspell pipe list --encoding=utf-8 --mode=ccpp --run-together --lang=en_US " + aspellIgnoreDictPathArg + "< " +
638                                                                  fullFilePath + " | grep \"\w\+ [0-9]\+ [0-9]\+:\" || true'", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
639                                                 stdoutOutput,stderrOutput = aspell.communicate()
640
641                                                 if aspell.returncode != 0 or stderrOutput:
642                                                         return False, "Error from aspell (error code:" + str(aspell.returncode) + "):\n" + stderrOutput
643
644                                                 foundWords, longestOutputLine = getDataFromAspellOutput(stdoutOutput)
645                                                 if len(foundWords) > 0:
646                                                         aspellSummary += "\nASPELL CHECK FILE: " + relativeFilePath + "\n"
647                                                 if stderrOutput:
648                                                         aspellSummary += "Something appeared on stderr: " + stderrOutput + "\n"
649                                                 for wordInfo in foundWords:
650                                                         lineWithWordUppercased = (wordInfo.lineStr[:wordInfo.positionInLine] + wordInfo.lineStr[wordInfo.positionInLine:wordInfo.positionInLine+len(wordInfo.wordStr)].upper() + wordInfo.lineStr[wordInfo.positionInLine + len(wordInfo.wordStr):]).strip()
651                                                         aspellSummary += (str(wordInfo.lineNumber + 1).rjust(5, " ") + ": " + wordInfo.wordStr.ljust(longestOutputLine + 1, " ") + str(wordInfo.positionInLine) + "-> " + lineWithWordUppercased +
652                                                                                                                 "\n" + "(".rjust(5 + 2 + longestOutputLine + 1 + 4, " ") + wordInfo.hintsStr + ")" + "\n")
653                                                 if ignoredWordsList is not None and aspellDictionaryObject is not None:
654                                                         aspellWithoutDict = subprocess.Popen("bash -c 'set -o pipefail; aspell pipe list --encoding=utf-8 --mode=ccpp --run-together --lang=en_US < " + fullFilePath + " | grep \"\w\+ [0-9]\+ [0-9]\+:\" || true'", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
655                                                         stdoutOutput,stderrOutput = aspellWithoutDict.communicate()
656                                                         if aspellWithoutDict.returncode != 0 or stderrOutput:
657                                                                 return False, "Error from aspell (error code:" + str(aspellWithoutDict.returncode) + "):\n" + stderrOutput
658                                                         foundWords, longestOutputLine = getDataFromAspellOutput(stdoutOutput)
659                                                         for wordInfo in foundWords:
660                                                                 for item in aspellDictionaryObject:
661                                                                         if item["word"] == wordInfo.wordStr:
662                                                                                 ignoredWordsList.append([relativeFilePath, wordInfo.wordStr, wordInfo.lineNumber, item["type"]])
663                                                                                 break
664
665                         return len(aspellSummary) == 0, aspellSummary
666                 except subprocess.CalledProcessError as err:
667                         return False, traceback.format_exc() + "\noutput:" + err.output
668
669         def checkSampleUsingCheckpatchTizen(self, nativeSampleChange, changeInfo):
670                 print "========> CHECKPATCH_TIZEN for ", nativeSampleChange.nativeSample.projectName, " and changeId:", nativeSampleChange.changeId
671                 curDir = os.getcwd()
672                 try:
673                         if changeInfo is None:
674                                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
675
676                         os.chdir(nativeSampleChange.nativeSample.projectPath)
677                         self._cleanRepoAndCheckoutToRevision(sampleChange = nativeSampleChange)
678                         res, output = self.invokeCheckpatchTizen()
679                         if res:
680                                 print output
681                                 print "SUCCESS: sources checked with checkpatch_tizen.pl for " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
682                         else:
683                                 print "ERROR:", output
684                                 print "FAIL: sources checked failed with checkpatch_tizen.pl for " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
685                                 return False, output
686                 finally:
687                         os.chdir(curDir)
688                 return True, ""
689         def checkSampleUsingAspell(self, nativeSampleChange, changeInfo):
690                 print "========> ASPELL_CHECK for ", nativeSampleChange.nativeSample.projectName, " and changeId:", nativeSampleChange.changeId
691                 curDir = os.getcwd()
692                 try:
693                         if changeInfo is None:
694                                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
695                         os.chdir(nativeSampleChange.nativeSample.projectPath)
696                         self._cleanRepoAndCheckoutToRevision(sampleChange = nativeSampleChange)
697                         res, output = self.invokeAspell()
698                         if res:
699                                 print output
700                                 print "SUCCESS: sources checked with aspell for " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
701                         else:
702                                 print "ERROR:", output
703                                 print "FAIL: sources checked failed with aspell for " + nativeSampleChange.changeId + " from project " + nativeSampleChange.nativeSample.projectName
704                                 return False, output
705                 finally:
706                         os.chdir(curDir)
707                 return True, ""
708         def evaluateChange(self, nativeSampleChange):
709                 print 'evaluating change:', nativeSampleChange
710                 curDir = os.getcwd()
711                 os.chdir(nativeSampleChange.nativeSample.projectPath)
712
713                 self._cleanRepoAndCheckoutToRevision(sampleChange = nativeSampleChange)
714                 changeInfo = self.gerrit.getChangeInfo(nativeSampleChange)
715
716                 if changeInfo is not None:
717                         if changeInfo['status'] != "MERGED" and changeInfo['status'] != "ABANDONED":
718                                 nativeSampleChange.changeId = changeInfo['id']
719
720                                 if self._currentSourceType() == self.SourceTypeTpkProject:
721                                         result, message = self.buildSampleFromTpkBranch(nativeSampleChange, changeInfo)
722                                         if result:
723                                                 self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: Compilation successful")
724                                         else:
725                                                 self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: Compilation failed:\n"+message)
726                                 elif self._currentSourceType() == self.SourceTypeSampleProject:
727                                         result, message = self.buildSampleFromProjectBranch(nativeSampleChange, changeInfo)
728                                         if result:
729                                                 self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: Project creation and compilation successful")
730                                         else:
731                                                 self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: Project creation and/or compilation failed:\n"+message)
732
733                                 result, message = self.checkSampleUsingTcc(nativeSampleChange, changeInfo)
734                                 if result:
735                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: TCC Check successful")
736                                 else:
737                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: TCC Check failed:\n"+message)
738
739                                 result, message = self.checkSampleUsingCheckpatchTizen(nativeSampleChange, changeInfo)
740                                 if result:
741                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: checkpatch_tizen successful")
742                                 else:
743                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: checkpatch_tizen failed:\n"+message)
744
745                                 result, message = self.checkSampleUsingAspell(nativeSampleChange, changeInfo)
746                                 if result:
747                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: spelling check successful")
748                                 else:
749                                         self.gerrit.addCommentToChange(nativeSampleChange, "BuildBot: fix spelling check errors or add words to ignore list:\n"+message)
750
751                         else:
752                                 print "Change already " + changeInfo['status']
753                 else:
754                         subject = "Can't find change id for change with revision : " + nativeSampleChange.revisionId + " of project:" + nativeSampleChange.nativeSample.projectName
755                         print subject
756                         self.emailSender.send(mailSubject = subject)
757                 os.chdir(curDir)
758
759         def evaluateProject(self, nativeSample):
760                 print "evaluating project", nativeSample.projectName
761                 curDir = os.getcwd()
762                 def convertResult(resultList):
763                         ret = {'result': False, 'comment': None}
764                         ret['result'] = resultList[0]
765                         print resultList[1]
766                         if resultList[0]:
767                                 ret['comment'] = "OK"
768                         else:
769                                 ret['comment'] = resultList[1]
770                         return ret
771                 try:
772                         os.chdir(nativeSample.projectPath)
773                         subprocess.call(["git", "fetch", "origin"], stderr=subprocess.STDOUT)
774
775                         ret = {
776                                 'SAMPLE_PROJECT_BUILD': {'result': False, 'comment': ""},
777                                 'TPK_BUILD': {'result': False, 'comment': ""},
778                                 'TCC_CHECK': {'result': False, 'comment': ""},
779                                 'CHECKPATCH_TIZEN': {'result': False, 'comment': ""},
780                                 'ASPELL_CHECK': {'result': False, 'comment': ""},
781                                 'CLOC_CHECK': {'result': False, 'comment': ""}
782                                 }
783
784                         self._cleanRepoAndCheckoutToRevision(repoPath = nativeSample.projectPath, revision="origin/tizen_2.4")
785                         #now we are on tizen_2.4 branch
786                         print '=======> SAMPLE_PROJECT_BUILD for ', nativeSample.projectName
787                         ret["SAMPLE_PROJECT_BUILD"] = convertResult(self.buildTpkFromProject(nativeSample, "daily_build_origin_tizen_2.4"))
788
789                         print '=======> invoking TCC_CHECK ', nativeSample.projectName
790                         ret["TCC_CHECK"] = convertResult(self.invokeTcc())
791
792                         print '=======> CHECKPATCH_TIZEN ', nativeSample.projectName
793                         ret["CHECKPATCH_TIZEN"] = convertResult(self.invokeCheckpatchTizen())
794
795                         print '=======> ASPELL_CHECK ', nativeSample.projectName
796                         ignoredWordsList = []
797                         ret["ASPELL_CHECK"] = convertResult(self.invokeAspell(ignoredWordsList))
798                         ignoredWordsCsvStr = ""
799                         for ignoredWord in ignoredWordsList:
800                                 dirname, fileName = os.path.split(ignoredWord[0])
801                                 ignoredWordsCsvStr += ",".join([dirname, fileName, ignoredWord[1], str(ignoredWord[2]), ignoredWord[3], ignoredWord[1]]) + "\n"
802                         ret["ASPELL_CHECK"]["comment"] = ret["ASPELL_CHECK"]["comment"] + "\nINGORED_LIST_CSV:\n" + ignoredWordsCsvStr
803
804                         self._cleanRepoAndCheckoutToRevision(repoPath = nativeSample.projectPath, revision="origin/tpk")
805                         #now we are on tpk branch - let's get first change
806                         print '=======> COMMENT_RATIO for ', nativeSample.projectName
807                         resultList = list(self.invokeCloc())
808                         languagesOutputList = resultList[2]
809                         codeLinesCount = 0
810                         commentLinesCount = 0
811                         csvOutput = []
812                         for languagesOutput in languagesOutputList:
813                                 commentLinesCount += float(languagesOutput[3])
814                                 codeLinesCount += float(languagesOutput[4])
815                                 tmp = languagesOutput[0]
816                                 languagesOutput[0] = languagesOutput[1]
817                                 languagesOutput[1] = tmp
818                                 csvOutput.append(",".join(languagesOutput))
819                         commentRatio = round(commentLinesCount*100/(commentLinesCount + codeLinesCount), 2)
820                         if commentRatio > 30:
821                                 ret["CLOC_CHECK"]["result"] = True
822                                 ret["CLOC_CHECK"]["comment"] = "\n" + "COMMENT_RATIO: " + str(commentRatio) + "%\nCLOC_OUTPUT_CSV:\n" + "\n".join(csvOutput)
823                         else:
824                                 ret["CLOC_CHECK"]["result"] = False
825                                 ret["CLOC_CHECK"]["comment"] = "\n" + "COMMENT_RATIO: " + str(commentRatio) + "%\n" + resultList[1]
826                         print ret["CLOC_CHECK"]["comment"]
827                         print '=======> TPK_BUILD for ', nativeSample.projectName
828                         ret["TPK_BUILD"] = convertResult(self.buildTpk())
829                 finally:
830                         os.chdir(curDir)
831                 return ret
832         def evaluatePendingChanges(self):
833                 changesList = self.databaseModel.getPendingNativeSampleChanges(self)
834
835                 for i in range(len(changesList)):
836                         try:
837                                 changesList[i].nativeSample = self._getNativeSample(changesList[i].nativeSample.projectName)
838                                 self.evaluateChange(changesList[i])
839                                 self.databaseModel.deleteNativeSampleChange(changesList[i])
840                         except KeyboardInterrupt:
841                                 raise
842                         except:
843                                 subject = 'Tizen SAMPLE BUILD SYSTEM error: Something unexpected happened during build process'
844                                 stacktrace = "Exception Info:\n\n" + traceback.format_exc()
845                                 traceback.print_exc()
846                                 self.emailSender.send(mailSubject = subject, mailText = stacktrace)
847                                 print "Evaluating next change"
848         def dailyRegressionCheck(self):
849                 htmlText = "<HTML><TABLE border=\"1\" style=\"width:80%\"><TR><TH>Project Name</TH><TH>Check step</TH><TH>Result</TH><TH>Comment</TH></TR>"
850                 def escapeHtml(s):
851                         return  s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
852                 for sampleProjectName in self.samplesList:
853                         try:
854                                 nativeSample = self._getNativeSample(sampleProjectName)
855                                 if not os.path.exists(nativeSample.projectPath):
856                                         self._cloneSampleFromGerrit(nativeSample)
857                                 res = self.evaluateProject(nativeSample)
858                                 spanText = "<TD rowspan=\"" + str(len(res.keys())) + "\">%s</TD>" % sampleProjectName
859                                 res.keys().sort()
860                                 for i, checkStep in enumerate(res.keys()):
861                                         htmlText += "<TR>"
862                                         if i == 0:
863                                                 htmlText += spanText
864                                         htmlText += ("<TD>%s</TD><TD>%i</TD><TD><PRE>%s</PRE></TD></TR>") % (checkStep, res[checkStep]['result'], escapeHtml(res[checkStep]['comment']))
865                         except KeyboardInterrupt:
866                                 raise
867                         except:
868                                 subject = 'Tizen DAILY REGRESSION BUILD SYSTEM error: Something unexpected happened during daily build process'
869                                 stacktrace = "Exception Info:\n\n" + traceback.format_exc()
870                                 traceback.print_exc()
871                                 self.emailSender.send(mailSubject = subject, mailText = stacktrace)
872                                 print "Evaluating next project"
873                 htmlText += "</TABLE></HTML>"
874                 self.emailSender.send(mailSubject = 'DAILY REGRESSION TESTS SUMMARY (' + str(datetime.date.today())+")", mailText = htmlText, mimeType='html')
875         def checkAndSendEmailIfUpdateIsAvailable(self):
876                 os.chdir(self.config.sampleCoreComponentsPath)
877                 subprocess.call(["git", "fetch", "origin"])
878                 output = subprocess.check_output(["git", "branch", "--contains", "origin/master"], stderr = subprocess.STDOUT)
879                 reRes =  re.search("^\*", output)
880                 if reRes is None:
881                         self.emailSender.send(mailSubject = "BUILDBOT UPDATE AVAILABLE", mailText = "Seems like update for sample-core-components is available. origin/master is not pulled to local repository")