blob: b97c3d23a85cbf8b7019887e85a28fd21e061738 [file] [log] [blame]
showard26b7ec72009-12-21 22:43:57 +00001"""\
2Functions to expose over the RPC interface.
3"""
4
5__author__ = 'jamesren@google.com (James Ren)'
6
7
jamesren9a6f5f62010-05-05 22:55:54 +00008import os, re
jamesrenc3940222010-02-19 21:57:37 +00009import common
10from django.db import models as django_models
11from autotest_lib.frontend import thread_local
12from autotest_lib.frontend.afe import model_logic, models as afe_models
13from autotest_lib.frontend.afe import rpc_utils as afe_rpc_utils
jamesren9a6f5f62010-05-05 22:55:54 +000014from autotest_lib.frontend.afe import rpc_interface as afe_rpc_interface
jamesren3e9f6092010-03-11 21:32:10 +000015from autotest_lib.frontend.tko import models as tko_models
jamesrenb852bce2010-04-07 20:36:13 +000016from autotest_lib.frontend.planner import models, rpc_utils, model_attributes
jamesren4be631f2010-04-08 23:01:22 +000017from autotest_lib.frontend.planner import failure_actions
jamesrenc3940222010-02-19 21:57:37 +000018from autotest_lib.client.common_lib import utils
19
20# basic getter/setter calls
21# TODO: deprecate the basic calls and reimplement them in the REST framework
22
23def get_plan(id):
24 return afe_rpc_utils.prepare_for_serialization(
25 models.Plan.smart_get(id).get_object_dict())
26
27
28def modify_plan(id, **data):
29 models.Plan.smart_get(id).update_object(data)
30
31
jamesren3e9f6092010-03-11 21:32:10 +000032def modify_test_run(id, **data):
33 models.TestRun.objects.get(id=id).update_object(data)
34
35
36def modify_host(id, **data):
37 models.Host.objects.get(id=id).update_object(data)
38
39
jamesren3e9f6092010-03-11 21:32:10 +000040def add_job(plan_id, test_config_id, afe_job_id):
41 models.Job.objects.create(
42 plan=models.Plan.objects.get(id=plan_id),
43 test_config=models.TestConfig.objects.get(id=test_config_id),
44 afe_job=afe_models.Job.objects.get(id=afe_job_id))
45
46
jamesrenc3940222010-02-19 21:57:37 +000047# more advanced calls
48
jamesren9a6f5f62010-05-05 22:55:54 +000049def submit_plan(name, hosts, host_labels, tests, support=None,
50 label_override=None, additional_parameters=None):
jamesrenc3940222010-02-19 21:57:37 +000051 """
52 Submits a plan to the Test Planner
53
54 @param name: the name of the plan
55 @param hosts: a list of hostnames
56 @param host_labels: a list of host labels. The hosts under test will update
57 to reflect changes in the label
jamesren3e9f6092010-03-11 21:32:10 +000058 @param tests: an ordered list of dictionaries:
59 alias: an alias for the test
60 control_file: the test control file
61 is_server: True if is a server-side control file
62 estimated_runtime: estimated number of hours this test
63 will run
jamesrendbeebf82010-04-08 22:58:26 +000064 @param support: the global support script
jamesrenc3940222010-02-19 21:57:37 +000065 @param label_override: label to prepend to all AFE jobs for this test plan.
66 Defaults to the plan name.
jamesren9a6f5f62010-05-05 22:55:54 +000067 @param additional_parameters: A mapping of AdditionalParameters to apply to
68 this test plan, as an ordered list. Each item
69 of the list is a dictionary:
70 hostname_regex: A regular expression; the
71 additional parameter in the
72 value will be applied if the
73 hostname matches this regex
74 param_type: The type of additional parameter
75 param_values: A dictionary of key=value pairs
76 for this parameter
77 example:
78 [{'hostname_regex': 'host[0-9]',
79 'param_type': 'Verify',
80 'param_values': {'key1': 'value1',
81 'key2': 'value2'}},
82 {'hostname_regex': '.*',
83 'param_type': 'Verify',
84 'param_values': {'key': 'value'}}]
85
86 Currently, the only (non-site-specific)
87 param_type available is 'Verify'. Setting
88 these parameters allows the user to specify
89 arguments to the
90 job.run_test('verify_test', ...) line at the
91 beginning of the wrapped control file for each
92 test
jamesrenc3940222010-02-19 21:57:37 +000093 """
94 host_objects = []
95 label_objects = []
96
97 for host in hosts or []:
98 try:
99 host_objects.append(
100 afe_models.Host.valid_objects.get(hostname=host))
101 except afe_models.Host.DoesNotExist:
102 raise model_logic.ValidationError(
103 {'hosts': 'host %s does not exist' % host})
104
105 for label in host_labels or []:
106 try:
107 label_objects.append(afe_models.Label.valid_objects.get(name=label))
108 except afe_models.Label.DoesNotExist:
109 raise model_logic.ValidationError(
110 {'host_labels': 'host label %s does not exist' % label})
111
jamesren3e9f6092010-03-11 21:32:10 +0000112 aliases_seen = set()
113 test_required_fields = (
114 'alias', 'control_file', 'is_server', 'estimated_runtime')
115 for test in tests:
116 for field in test_required_fields:
117 if field not in test:
118 raise model_logic.ValidationError(
119 {'tests': 'field %s is required' % field})
120
121 alias = test['alias']
122 if alias in aliases_seen:
123 raise model_logic.Validationerror(
124 {'tests': 'alias %s occurs more than once' % alias})
125 aliases_seen.add(alias)
126
jamesrenc3940222010-02-19 21:57:37 +0000127 plan, created = models.Plan.objects.get_or_create(name=name)
128 if not created:
129 raise model_logic.ValidationError(
130 {'name': 'Plan name %s already exists' % name})
131
132 try:
jamesren9a6f5f62010-05-05 22:55:54 +0000133 rpc_utils.set_additional_parameters(plan, additional_parameters)
jamesrenc3940222010-02-19 21:57:37 +0000134 label = rpc_utils.create_plan_label(plan)
jamesren3e9f6092010-03-11 21:32:10 +0000135 try:
136 for i, test in enumerate(tests):
137 control, _ = models.ControlFile.objects.get_or_create(
138 contents=test['control_file'])
139 models.TestConfig.objects.create(
140 plan=plan, alias=test['alias'], control_file=control,
141 is_server=test['is_server'], execution_order=i,
142 estimated_runtime=test['estimated_runtime'])
143
144 plan.label_override = label_override
145 plan.support = support or ''
146 plan.save()
147
148 plan.owners.add(afe_models.User.current_user())
149
150 for host in host_objects:
151 planner_host = models.Host.objects.create(plan=plan, host=host)
152
153 plan.host_labels.add(*label_objects)
154
155 rpc_utils.start_plan(plan, label)
156
157 return plan.id
158 except:
159 label.delete()
160 raise
jamesrenc3940222010-02-19 21:57:37 +0000161 except:
162 plan.delete()
163 raise
164
jamesrenc3940222010-02-19 21:57:37 +0000165
166def get_hosts(plan_id):
167 """
168 Gets the hostnames of all the hosts in this test plan.
169
170 Resolves host labels in the plan.
171 """
172 plan = models.Plan.smart_get(plan_id)
173
174 hosts = set(plan.hosts.all().values_list('hostname', flat=True))
175 for label in plan.host_labels.all():
176 hosts.update(label.host_set.all().values_list('hostname', flat=True))
177
178 return afe_rpc_utils.prepare_for_serialization(hosts)
179
180
181def get_atomic_group_control_file():
182 """
183 Gets the control file to apply the atomic group for a set of machines
184 """
185 return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__),
186 'set_atomic_group_control.srv'))
jamesren3e9f6092010-03-11 21:32:10 +0000187
188
189def get_next_test_configs(plan_id):
190 """
191 Gets information about the next planner test configs that need to be run
192
193 @param plan_id: the ID or name of the test plan
194 @return a dictionary:
195 complete: True or False, shows test plan completion
196 next_configs: a list of dictionaries:
197 host: ID of the host
198 next_test_config_id: ID of the next Planner test to run
199 """
200 plan = models.Plan.smart_get(plan_id)
201
202 result = {'next_configs': []}
203
204 rpc_utils.update_hosts_table(plan)
205 for host in models.Host.objects.filter(plan=plan):
jamesrendbeebf82010-04-08 22:58:26 +0000206 next_test_config = rpc_utils.compute_next_test_config(plan, host)
207 if next_test_config:
208 config = {'next_test_config_id': next_test_config.id,
209 'next_test_config_alias': next_test_config.alias,
jamesren3e9f6092010-03-11 21:32:10 +0000210 'host': host.host.hostname}
211 result['next_configs'].append(config)
212
213 rpc_utils.check_for_completion(plan)
214 result['complete'] = plan.complete
215
216 return result
217
218
219def update_test_runs(plan_id):
220 """
221 Add all applicable TKO jobs to the Planner DB tables
222
223 Looks for tests in the TKO tables that were started as a part of the test
224 plan, and add them to the Planner tables.
225
226 Also updates the status of the test run if the underlying TKO test move from
227 an active status to a completed status.
228
229 @return a list of dictionaries:
230 status: the status of the new (or updated) test run
231 tko_test_idx: the ID of the TKO test added
232 hostname: the host added
233 """
jamesren4be631f2010-04-08 23:01:22 +0000234 plan = models.Plan.smart_get(plan_id)
jamesren3e9f6092010-03-11 21:32:10 +0000235 updated = []
236
237 for planner_job in plan.job_set.all():
238 known_statuses = dict((test_run.tko_test.test_idx, test_run.status)
239 for test_run in planner_job.testrun_set.all())
240 tko_tests_for_job = tko_models.Test.objects.filter(
241 job__afe_job_id=planner_job.afe_job.id)
242
243 for tko_test in tko_tests_for_job:
244 status = rpc_utils.compute_test_run_status(tko_test.status.word)
245 needs_update = (tko_test.test_idx not in known_statuses or
246 status != known_statuses[tko_test.test_idx])
247 if needs_update:
248 hostnames = tko_test.machine.hostname.split(',')
249 for hostname in hostnames:
250 rpc_utils.add_test_run(
251 plan, planner_job, tko_test, hostname, status)
252 updated.append({'status': status,
253 'tko_test_idx': tko_test.test_idx,
254 'hostname': hostname})
255
256 return updated
jamesrenb852bce2010-04-07 20:36:13 +0000257
258
259def get_failures(plan_id):
260 """
261 Gets a list of the untriaged failures associated with this plan
262
263 @return a list of dictionaries:
264 id: the failure ID, for passing back to triage the failure
265 group: the group for the failure. Normally the same as the
266 reason, but can be different for custom queries
267 machine: the failed machine
268 blocked: True if the failure caused the machine to block
269 test_name: Concatenation of the Planner alias and the TKO test
270 name for the failed test
271 reason: test failure reason
272 seen: True if the failure is marked as "seen"
273 """
274 plan = models.Plan.smart_get(plan_id)
275 result = {}
276
277 failures = plan.testrun_set.filter(
278 finalized=True, triaged=False,
279 status=model_attributes.TestRunStatus.FAILED)
jamesren62758242010-04-28 18:08:25 +0000280 failures = failures.order_by('seen').select_related('test_job__test',
281 'host__host',
282 'tko_test')
jamesrenb852bce2010-04-07 20:36:13 +0000283 for failure in failures:
jamesren62758242010-04-28 18:08:25 +0000284 test_name = '%s: %s' % (
jamesrenb852bce2010-04-07 20:36:13 +0000285 failure.test_job.test_config.alias, failure.tko_test.test)
286
287 group_failures = result.setdefault(failure.tko_test.reason, [])
288 failure_dict = {'id': failure.id,
289 'machine': failure.host.host.hostname,
290 'blocked': bool(failure.host.blocked),
291 'test_name': test_name,
292 'reason': failure.tko_test.reason,
293 'seen': bool(failure.seen)}
294 group_failures.append(failure_dict)
295
296 return result
297
298
jamesrendbeebf82010-04-08 22:58:26 +0000299def get_test_runs(**filter_data):
300 """
301 Gets a list of test runs that match the filter data.
302
303 Returns a list of expanded TestRun object dictionaries. Specifically, the
304 "host" and "test_job" fields are expanded. Additionally, the "test_config"
305 field of the "test_job" expansion is also expanded.
306 """
307 result = []
308 for test_run in models.TestRun.objects.filter(**filter_data):
309 test_run_dict = test_run.get_object_dict()
310 test_run_dict['host'] = test_run.host.get_object_dict()
311 test_run_dict['test_job'] = test_run.test_job.get_object_dict()
312 test_run_dict['test_job']['test_config'] = (
313 test_run.test_job.test_config.get_object_dict())
314 result.append(test_run_dict)
315 return result
316
317
318def skip_test(test_config_id, hostname):
319 """
320 Marks a test config as "skipped" for a given host
321 """
322 config = models.TestConfig.objects.get(id=test_config_id)
323 config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname))
324
325
jamesren4be631f2010-04-08 23:01:22 +0000326def mark_failures_as_seen(failure_ids):
327 """
328 Marks a set of failures as 'seen'
329
330 @param failure_ids: A list of failure IDs, as returned by get_failures(), to
331 mark as seen
332 """
333 models.TestRun.objects.filter(id__in=failure_ids).update(seen=True)
334
335
jamesren62758242010-04-28 18:08:25 +0000336def process_failures(failure_ids, host_action, test_action, labels=(),
337 keyvals=None, bugs=(), reason=None, invalidate=False):
jamesren4be631f2010-04-08 23:01:22 +0000338 """
339 Triage a failure
340
341 @param failure_id: The failure ID, as returned by get_failures()
342 @param host_action: One of 'Block', 'Unblock', 'Reinstall'
343 @param test_action: One of 'Skip', 'Rerun'
344
345 @param labels: Test labels to apply, by name
346 @param keyvals: Dictionary of job keyvals to add (or replace)
347 @param bugs: List of bug IDs to associate with this failure
348 @param reason: An override for the test failure reason
349 @param invalidate: True if failure should be invalidated for the purposes of
350 reporting. Defaults to False.
351 """
jamesren4be631f2010-04-08 23:01:22 +0000352 host_choices = failure_actions.HostAction.values
353 test_choices = failure_actions.TestAction.values
354 if host_action not in host_choices:
355 raise model_logic.ValidationError(
356 {'host_action': ('host action %s not valid; must be one of %s'
357 % (host_action, ', '.join(host_choices)))})
358 if test_action not in test_choices:
359 raise model_logic.ValidationError(
360 {'test_action': ('test action %s not valid; must be one of %s'
361 % (test_action, ', '.join(test_choices)))})
362
jamesren62758242010-04-28 18:08:25 +0000363 for failure_id in failure_ids:
364 rpc_utils.process_failure(
365 failure_id=failure_id, host_action=host_action,
366 test_action=test_action, labels=labels, keyvals=keyvals,
367 bugs=bugs, reason=reason, invalidate=invalidate)
jamesren4be631f2010-04-08 23:01:22 +0000368
369
jamesren06e59f12010-05-24 17:05:06 +0000370def get_machine_view_data(plan_id):
371 """
372 Gets the data required for the web frontend Machine View.
373
374 @param plan_id: The ID of the test plan
375 @return An array. Each element is a dictionary:
376 machine: The name of the machine
377 status: The machine's status (one of
378 model_attributes.HostStatus)
379 bug_ids: List of the IDs for the bugs filed
380 tests_run: An array of dictionaries:
381 test_name: The TKO name of the test
382 success: True if the test passed
383 """
384 plan = models.Plan.smart_get(plan_id)
385 result = []
386 for host in plan.host_set.all():
387 tests_run = []
388
389 machine = host.host.hostname
390 host_status = host.status()
391 bug_ids = set()
392
393 testruns = plan.testrun_set.filter(host=host, invalidated=False,
394 finalized=True)
395 for testrun in testruns:
396 test_name = testrun.tko_test.test
397 test_status = testrun.tko_test.status.word
398 testrun_bug_ids = testrun.bugs.all().values_list(
399 'external_uid', flat=True)
400
401 tests_run.append({'test_name': test_name,
402 'status': test_status})
403 bug_ids.update(testrun_bug_ids)
404
405 result.append({'machine': machine,
406 'status': host_status,
407 'tests_run': tests_run,
408 'bug_ids': list(bug_ids)})
409 return result
410
411
jamesren9a6f5f62010-05-05 22:55:54 +0000412def generate_test_config(alias, afe_test_name=None,
413 estimated_runtime=0, **kwargs):
414 """
415 Creates and returns a test config suitable for passing into submit_plan()
416
417 Also accepts optional parameters to pass directly in to the AFE RPC
418 interface's generate_control_file() method.
419
420 @param alias: The alias for the test
421 @param afe_test_name: The name of the test, as shown on AFE
422 @param estimated_runtime: Estimated number of hours this test is expected to
423 run. For reporting purposes.
424 """
425 if afe_test_name is None:
426 afe_test_name = alias
427 alias = alias.replace(' ', '_')
428
429 control = afe_rpc_interface.generate_control_file(tests=[afe_test_name],
430 **kwargs)
431
432 return {'alias': alias,
433 'control_file': control['control_file'],
434 'is_server': control['is_server'],
435 'estimated_runtime': estimated_runtime}
436
437
438def get_wrapped_test_config(id, hostname, run_verify):
439 """
440 Gets the TestConfig object identified by the ID
441
442 Returns the object dict of the TestConfig, plus an additional
443 'wrapped_control_file' value, which includes the pre-processing that the
444 ControlParameters specify.
445
446 @param hostname: Hostname of the machine this test config will run on
447 @param run_verify: Set to True or False to override the default behavior
448 (which is to run the verify test unless the skip_verify
449 ControlParameter is set)
450 """
451 test_config = models.TestConfig.objects.get(id=id)
452 object_dict = test_config.get_object_dict()
453 object_dict['control_file'] = test_config.control_file.get_object_dict()
454 object_dict['wrapped_control_file'] = rpc_utils.wrap_control_file(
455 plan=test_config.plan, hostname=hostname,
456 run_verify=run_verify, test_config=test_config)
457
458 return object_dict
459
460
461def generate_additional_parameters(hostname_regex, param_type, param_values):
462 """
463 Generates an AdditionalParamter dictionary, for passing in to submit_plan()
464
465 Returns a dictionary. To use in submit_job(), put this dictionary into a
466 list (possibly with other additional_parameters dictionaries)
467
468 @param hostname_regex: The hostname regular expression to match
469 @param param_type: One of get_static_data()['additional_parameter_types']
470 @param param_values: Dictionary of key=value pairs for this parameter
471 """
472 try:
473 re.compile(hostname_regex)
474 except Exception:
475 raise model_logic.ValidationError(
476 {'hostname_regex': '%s is not a valid regex' % hostname_regex})
477
478 if param_type not in model_attributes.AdditionalParameterType.values:
479 raise model_logic.ValidationError(
480 {'param_type': '%s is not a valid parameter type' % param_type})
481
482 if type(param_values) is not dict:
483 raise model_logic.ValidationError(
484 {'param_values': '%s is not a dictionary' % repr(param_values)})
485
486 return {'hostname_regex': hostname_regex,
487 'param_type': param_type,
488 'param_values': param_values}
489
490
jamesren2e48bcb2010-05-18 20:41:36 +0000491def get_overview_data(plan_ids):
492 """
493 Gets the data for the Overview tab
494
495 @param plan_ids: A list of the plans, by id or name
496 @return A dictionary - keys are plan names, values are dictionaries of data:
497 machines: A list of dictionaries:
498 hostname: The machine's hostname
499 status: The host's status
500 passed: True if the machine passed the test plan. A 'pass' means
501 that, for every test configuration in the plan, the
502 machine had at least one AFE job with no failed tests.
503 'passed' could also be None, meaning that this host is
504 still running tests.
505 bugs: A list of the bugs filed
506 test_configs: A list of dictionaries, each representing a test
507 config:
508 complete: Number of hosts that have completed this test
509 config
510 estimated_runtime: Number of hours this test config is
511 expected to run on each host
512 """
513 plans = models.Plan.smart_get_bulk(plan_ids)
514 result = {}
515
516 for plan in plans:
517 machines = []
518 for host in plan.host_set.all():
jamesren06e59f12010-05-24 17:05:06 +0000519 pass_status = rpc_utils.compute_test_config_status(host)
520 if pass_status == rpc_utils.ComputeTestConfigStatusResult.PASS:
521 passed = True
522 elif pass_status == rpc_utils.ComputeTestConfigStatusResult.FAIL:
523 passed = False
524 else:
525 passed = None
jamesren2e48bcb2010-05-18 20:41:36 +0000526 machines.append({'hostname': host.host.hostname,
527 'status': host.status(),
jamesren06e59f12010-05-24 17:05:06 +0000528 'passed': passed})
jamesren2e48bcb2010-05-18 20:41:36 +0000529
530 bugs = set()
531 for testrun in plan.testrun_set.all():
532 bugs.update(testrun.bugs.values_list('external_uid', flat=True))
533
534 test_configs = []
535 for test_config in plan.testconfig_set.all():
jamesren2e48bcb2010-05-18 20:41:36 +0000536 complete_jobs = test_config.job_set.filter(
jamesren06e59f12010-05-24 17:05:06 +0000537 afe_job__hostqueueentry__complete=True)
jamesren2e48bcb2010-05-18 20:41:36 +0000538 complete_afe_jobs = afe_models.Job.objects.filter(
539 id__in=complete_jobs.values_list('afe_job', flat=True))
540
541 complete_hosts = afe_models.Host.objects.filter(
542 hostqueueentry__job__in=complete_afe_jobs)
543 complete_hosts |= test_config.skipped_hosts.all()
544
545 test_configs.append(
546 {'complete': complete_hosts.distinct().count(),
547 'estimated_runtime': test_config.estimated_runtime})
548
549 plan_data = {'machines': machines,
550 'bugs': list(bugs),
551 'test_configs': test_configs}
552 result[plan.name] = plan_data
553
554 return result
555
556
jamesren06e59f12010-05-24 17:05:06 +0000557def get_test_view_data(plan_id):
558 """
559 Gets the data for the Test View tab
560
561 @param plan_id: The name or ID of the test plan
562 @return A dictionary - Keys are test config aliases, values are dictionaries
563 of data:
564 total_machines: Total number of machines scheduled for this test
565 config. Excludes machines that are set to skip
566 this config.
567 machine_status: A dictionary:
568 key: The hostname
569 value: The status of the machine: one of 'Scheduled',
570 'Running', 'Pass', or 'Fail'
571 total_runs: Total number of runs of this test config. Includes
572 repeated runs (from triage re-run)
573 total_passes: Number of runs that resulted in a 'pass', meaning
574 that none of the tests in the test config had any
575 status other than GOOD.
576 bugs: List of bugs that were filed under this test config
577 """
578 plan = models.Plan.smart_get(plan_id)
579 result = {}
580 for test_config in plan.testconfig_set.all():
581 skipped_host_ids = test_config.skipped_hosts.values_list('id',
582 flat=True)
583 hosts = plan.host_set.exclude(host__id__in=skipped_host_ids)
584 total_machines = hosts.count()
585
586 machine_status = {}
587 for host in hosts:
588 machine_status[host.host.hostname] = (
589 rpc_utils.compute_test_config_status(host, test_config))
590
591 planner_jobs = test_config.job_set.all()
592 total_runs = planner_jobs.count()
593 total_passes = 0
594 for planner_job in planner_jobs:
595 if planner_job.all_tests_passed():
596 total_passes += 1
597
598 test_runs = plan.testrun_set.filter(
599 test_job__in=test_config.job_set.all())
600 bugs = set()
601 for test_run in test_runs:
602 bugs.update(test_run.bugs.values_list('external_uid', flat=True))
603
604 result[test_config.alias] = {'total_machines': total_machines,
605 'machine_status': machine_status,
606 'total_runs': total_runs,
607 'total_passes': total_passes,
608 'bugs': list(bugs)}
609 return result
610
611
jamesren1602f3b2010-04-09 20:46:29 +0000612def get_motd():
613 return afe_rpc_utils.get_motd()
614
615
jamesrenb852bce2010-04-07 20:36:13 +0000616def get_static_data():
jamesren1602f3b2010-04-09 20:46:29 +0000617 result = {'motd': get_motd(),
jamesren4be631f2010-04-08 23:01:22 +0000618 'host_actions': sorted(failure_actions.HostAction.values),
jamesren9a6f5f62010-05-05 22:55:54 +0000619 'test_actions': sorted(failure_actions.TestAction.values),
620 'additional_parameter_types':
jamesren2e48bcb2010-05-18 20:41:36 +0000621 sorted(model_attributes.AdditionalParameterType.values),
622 'host_statuses': sorted(model_attributes.HostStatus.values)}
jamesrenb852bce2010-04-07 20:36:13 +0000623 return result