comparison host/infra/hosttest.exp @ 76:435cced73e2f ecos-v1_3_1-release

eCos v1.3.1 merged from eCos master repository on 2000-03-27-23:22:51-BST
author jlarmour
date Tue, 28 Mar 2000 14:10:45 +0000
parents
children f58bc1b2c2bc
comparison
equal deleted inserted replaced
75:41bf073c0c32 76:435cced73e2f
1 #===============================================================================
2 #
3 # hosttest.exp
4 #
5 # Support for host-side testing
6 #
7 #===============================================================================
8 ######COPYRIGHTBEGIN####
9 #
10 # ----------------------------------------------------------------------------
11 # Copyright (C) 1998, 1999, 2000 Red Hat, Inc.
12 #
13 # This file is part of the eCos host tools.
14 #
15 # This program is free software; you can redistribute it and/or modify it
16 # under the terms of the GNU General Public License as published by the Free
17 # Software Foundation; either version 2 of the License, or (at your option)
18 # any later version.
19 #
20 # This program is distributed in the hope that it will be useful, but WITHOUT
21 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
22 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
23 # more details.
24 #
25 # You should have received a copy of the GNU General Public License along with
26 # this program; if not, write to the Free Software Foundation, Inc.,
27 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 #
29 # ----------------------------------------------------------------------------
30 #
31 ######COPYRIGHTEND####
32 #===============================================================================
33 ######DESCRIPTIONBEGIN####
34 #
35 # Author(s): bartv
36 # Contributors: bartv
37 # Date: 1998-11-25
38 # Note: Arguably this should be a loadable package
39 #
40 #####DESCRIPTIONEND####
41 #===============================================================================
42 #
43
44 # ----------------------------------------------------------------------------
45 # This script gets loaded by host-side DejaGnu test harnesses to provide
46 # various utilities for testing eCos host applications. It lives in the
47 # host-side infrastructure directory and gets installed in
48 # $(PREFIX)/share/dejagnu.
49 #
50 # The script can assume that a number of globals from the site.exp
51 # file have been read in. These include:
52 # tool - name of the tool (i.e. the package)
53 # srcdir - location of the source directory
54 # objdir - location of the build tree
55 # host_alias - config triplet
56 # host_triplet - ditto
57 #
58 # The generated Makefile has some additional information that is useful.
59 # CC - name of the C compiler that is used
60 # CXX - name of the C++ compiler
61 # prefix - where everything gets installed
62 # OBJEXT - either o or obj
63 # EXEEXT - either nothing or .exe
64 # VERSION - the version number
65 # CFLAGS - flags to use when compiling C code
66 # CXXFLAGS - flags to use when compiling C++ code
67 #
68 # hosttest_initialize
69 # Perform any initialization steps that are needed. Currently this
70 # means reading in the Makefile from the top-level of the build tree
71 # and figuring out the values of CC, CXX, and anything else that is
72 # useful. Any errors should be reported via perror and then the
73 # script should exit.
74 #
75 # There is an optional argument, a list of additional variables which
76 # should be present in the makefile and whose values are desired.
77
78 proc hosttest_initialize { { pkg_vars {} } } {
79
80 # First check that this script is actually running inside DejaGnu
81 if { [info exists ::objdir] == 0 } {
82 puts "Variable ::objdir is not defined, is this script really running inside DejaGnu?"
83 exit 1
84 }
85
86 # The information is stored in an array hosttest_data. Make sure this
87 # array exists.
88 array set ::hosttest_data {}
89
90 # Now clear out any entries in the array
91 foreach entry [array names ::hosttest_data] {
92 unset ::hosttest_data($entry)
93 }
94
95 # Now read in the build tree's Makefile (and not the testsuite's Makefile)
96 set filename [file join $::objdir .. Makefile]
97 if { [file exists $filename] == 0 } {
98 perror "Initialization error: the build tree's Makefile $filename does not exist."
99 exit 1
100 }
101 set status [ catch {
102 set fd [open $filename r]
103 set contents [read $fd]
104 close $fd
105 } message]
106 if { $status != 0 } {
107 perror "Error reading $filename.\n$message"
108 exit 1
109 }
110
111 # The data is available. Search it for each of the variables of
112 # interest. Some variables are optional and are given default
113 # values.
114 set ::hosttest_data(CFLAGS) ""
115 set ::hosttest_data(CXXFLAGS) ""
116
117 set lines [split $contents "\n"]
118
119 foreach var [concat { CC CXX prefix OBJEXT EXEEXT VERSION CFLAGS CXXFLAGS } $pkg_vars] {
120
121 set pattern "^$var\[ \t\]*:?=\[ \t\]* (.*)\$"
122 set dummy ""
123 set match ""
124
125 foreach line $lines {
126 if { [regexp -- $pattern $line dummy match] == 1 } {
127 set ::hosttest_data($var) $match
128 break
129 }
130 }
131 if { [info exists ::hosttest_data($var)] == 0 } {
132 perror "Variable $var is not defined in $filename"
133 exit 1
134 }
135 }
136
137 # If compiling with VC++ remove any cygwin-isms from the prefix
138 if { [string match "cl*" $::hosttest_data(CC)] } {
139 set status [catch "exec cygpath -w $::hosttest_data(prefix)" message]
140 if { $status == 0 } {
141 regsub -all -- {\\} $message {/} ::hosttest_data(prefix)
142 } else {
143 perror "Converting cygwin pathname $::hosttest_data(prefix)\n$message"
144 exit 1
145 }
146 }
147 }
148
149
150 # ----------------------------------------------------------------------------
151 # hosttest_extract_version
152 # Assuming there has been a call to initialize, the required information
153 # should be available in the hosttest_data array. The initialize
154 # function should have aborted if the data is not available.
155
156 proc hosttest_extract_version { } {
157
158 if { [info exists ::hosttest_data(VERSION)] == 0 } {
159 error "No version information - host testing has not been properly initialized."
160 }
161
162 if { [info exists ::objdir] == 0 } {
163 error "Variable ::objdir is not defined, is this script really running inside DejaGnu?"
164 }
165 return $::hosttest_data(VERSION)
166 }
167
168 # ----------------------------------------------------------------------------
169 # hosttest_compile
170 # compile and link one or more source files. The arguments are:
171 # 1) the name of the test case
172 # 2) a list of one or more source files that need to be compiled.
173 # Both .c and .cxx files are supported, and the appropriate
174 # compiler will be used. If this list is empty then the
175 # code will look for a .c or a .cxx file which matches the
176 # name of the test executable. Source files are assumed to
177 # be relative to $::srcdir/$::subdir
178 # 3) a list (possibly empty) of directories that should be in the
179 # include path. The build tree's directory is automatically in
180 # the path, as is $(PREFIX)/include. Note that the build tree
181 # is actually one level above objdir, on the assumption that
182 # objdir is the testsuite subdirectory of the real objdir.
183 # 4) ditto for library search paths.
184 # 5) and a list of additional libraries that should be linked.
185 #
186 # Currently it is not possible to pass compiler flags since those
187 # might need translating between gcc and VC++. This may have to be
188 # resolved.
189 #
190 # Currently linking is not done via libtool. This may have to change.
191 #
192 # The various object files and the executable are placed in a directory
193 # testcase in the build tree, to avoid the risk of name clashes. This
194 # directory must not exist yet. There is a separate routine hosttest_clean
195 # which simply expunges the entire testcase directory.
196 #
197 # The output of a succesful compile or built is reported using
198 # verbose at level 2. Unsuccesful compiles or builts are reported using
199 # level 1.
200
201 proc hosttest_compile { name sources incdirs libdirs libs } {
202
203 # Make sure that the testcase directory does not yet exist, then
204 # create it. This guarantees a clean system and reasonable access
205 # permissions. Each testcase invocation should involve a call to
206 # the clean function.
207 set dirname [file join $::objdir "testcase"]
208 if { [file exists $dirname] != 0 } {
209 # An empty directory is ok.
210 if { [llength [glob -nocomplain -- [file join $dirname "*"]]] != 0 } {
211 error "hosttest_compile: $dirname already exists"
212 }
213 }
214
215 set status [catch { file mkdir $dirname } message]
216 if { $status != 0 } {
217 error "hosttest_compile: unable to create directory $dirname"
218 }
219
220 # The only argument that must be valid is the test name.
221 if { $name == "" } {
222 error "hosttest_compile: invalid test case name"
223 }
224
225 # If the list of sources is empty then look for a suitable
226 # file in the appropriate directory.
227 if { [llength $sources] == 0 } {
228 set filename [file join $::srcdir $::subdir "${name}.c"]
229 if { [file exists $filename] && [file isfile $filename] } {
230 lappend sources [file tail $filename]
231 } else {
232 set filename [file join $::srcdir $::subdir "${name}.cxx"]
233 if { [file exists $filename] && [file isfile $filename] } {
234 lappend sources [file tail $filename]
235 } else {
236 error "hosttest_compile: no sources listed and unable to find ${name}.c or ${name}.cxx"
237 }
238 }
239 }
240
241 # For each source file, generate a compile command line and try to execute
242 # it. The command line takes the form:
243 # (CC|CXX) -c (CFLAGS|CXXFLAGS) (INCDIRS) -o xxx yyy
244 #
245 # It is also useful to produce a list of the object files that need to
246 # linked later on, and to work out which tool should be invoked for
247 # linking.
248 set object_files {}
249 set has_cxx_files 0
250
251 foreach source $sources {
252 set commandline ""
253 if { [file extension $source] == ".c" } {
254 append commandline "$::hosttest_data(CC) -c $::hosttest_data(CFLAGS) "
255 } elseif { [file extension $source] == ".cxx" } {
256 set has_cxx_files 1
257 append commandline "$::hosttest_data(CXX) -c $::hosttest_data(CXXFLAGS) "
258 } else {
259 error "hosttest_compile: files of type [file extension $source] ($source) are not yet supported."
260 }
261
262 # Include path: start with the source tree. Then the build tree.
263 # Then any additional directories specified explicitly by the
264 # testcase. Finish off with the prefix. Note that header files
265 # in the prefix directory may be out of date, depending on whether
266 # or not there has been an install recently.
267 append commandline "-I[file join [pwd] [file dirname $::srcdir]] "
268 append commandline "-I[file join [pwd] [file dirname $::objdir]] "
269 foreach dir $incdirs {
270 append commandline "-I[file join [pwd] $dir] "
271 }
272 append commandline "-I[file join [pwd] $::hosttest_data(prefix) include] "
273
274 # The output file must go into the testcase directory and have the right suffix
275 set objfile "[file root [file tail $source]].$::hosttest_data(OBJEXT)"
276 lappend object_files $objfile
277 if { [string match "cl*" $::hosttest_data(CC)] } {
278 append commandline "-Fo[file join $::objdir testcase $objfile] "
279 } else {
280 append commandline "-o [file join $::objdir testcase $objfile] "
281 }
282
283 # Finally provide the source file.
284 append commandline "[file join $::srcdir $::subdir $source]"
285 verbose -log -- $commandline
286
287 # Time to invoke the compiler.
288 set status [catch { set result [eval exec -keepnewline -- $commandline] } message]
289 if { $status == 0 } {
290 # The compile succeeded and the output is in result. Report the
291 # output.
292 verbose -log -- $result
293 } else {
294 # The compile failed and the output is in message.
295 verbose -log -- $message
296 error "hosttest_compile: failed to compile $source"
297 }
298 }
299
300 # At this stage all the source files have been compiled, a list of
301 # object files has been constructed, and it is known whether or
302 # not any of the sources were c++. Time to construct a new command
303 # line.
304 set commandline ""
305 if { $has_cxx_files == 0 } {
306 append commandline "$::hosttest_data(CC) $::hosttest_data(CFLAGS) "
307 } else {
308 append commandline "$::hosttest_data(CXX) $::hosttest_data(CXXFLAGS) "
309 }
310 set exename [file join $::objdir "testcase" "$name$::hosttest_data(EXEEXT)"]
311
312 # List all of the object files
313 foreach obj $object_files {
314 append commandline "[file join $::objdir "testcase" $obj] "
315 }
316
317 # Now take care of libraries and search paths. This requires different
318 # code for gcc and VC++.
319
320 if { [string match "cl*" $::hosttest_data(CC)] } {
321 append commandline "-Fe$exename "
322
323 foreach lib $libs {
324 append commandline "${lib}.lib "
325 }
326 append commandline "-link "
327 append commandline "-libpath=[file join [pwd] [file dirname $::objdir]] "
328 foreach dir $libdirs {
329 append commandline "-libpath=[file join [pwd] $dir] "
330 }
331 append commandline "-libpath=[file join [pwd] $::hosttest_data(prefix) lib] "
332 } else {
333 append commandline "-o $exename "
334 append commandline "-L[file join [pwd] [file dirname $::objdir]] "
335 foreach dir $libdirs {
336 append commandline "-L[file join [pwd] $dir] "
337 }
338 append commandline "-L[file join [pwd] $::hosttest_data(prefix) lib] "
339 foreach lib $libs {
340 append commandline "-l$lib "
341 }
342 }
343
344 # We have a command line, go for it.
345 verbose -log -- $commandline
346 set status [catch { set result [eval exec -keepnewline -- $commandline] } message]
347 if { $status == 0 } {
348 # The link has succeeded, we have an executable.
349 verbose -log -- $result
350 } else {
351 # The link failed and the output is in message.
352 # Report things are per compilation failures
353 verbose -log -- $message
354 error "hosttest_compile: failed to link $exename"
355 }
356
357 # There should be a test executable.
358 }
359
360 # ----------------------------------------------------------------------------
361 # hosttest_clean
362 # Clean up a testcase directory.
363
364 proc hosttest_clean { } {
365
366 set dirname [file join $::objdir "testcase"]
367 if { [file exists $dirname] == 0 } {
368
369 # Something must have gone seriously wrong during the build phase,
370 # there is nothing there.
371 return
372 }
373
374 if { [file isdirectory $dirname] == 0 } {
375 error "hosttest_clean: $dirname should be a directory"
376 }
377
378 foreach entry [glob -nocomplain -- [file join $dirname "*"]] {
379 set filename [file join $dirname $entry]
380 if { [file isfile $filename] == 0 } {
381 error "hosttest_clean: $filename is not a file"
382 }
383 set status [catch { file delete -force -- $filename } message]
384 if { $status != 0 } {
385 error "hosttest_clean: unable to delete $filename, $message"
386 }
387 }
388 set status [catch { file delete -force -- $dirname } message]
389 if { $status != 0 } {
390 error "hosttest_clean: unable to delete directory $dirname, $message"
391 }
392 }
393
394 # ----------------------------------------------------------------------------
395 # Run a test executable, returning the status code and the output.
396 # The results are returned in variables. It is assumed that some test cases
397 # will fail, so raising an exception is appropriate only if something
398 # has gone wrong at the test harness level. The argument list
399 # should be the name of the test case (from which the executable file name
400 # can be derived) and a list of arguments.
401
402 proc hosttest_run { result_arg output_arg test args } {
403
404 upvar $result_arg result
405 upvar $output_arg output
406
407 # Figure out the filename corresponding to the test and make
408 # sure it exists.
409 set filename [file join $::objdir "testcase" $test]
410 append filename $::hosttest_data(EXEEXT)
411 if { ([file exists $filename] == 0) || ([file isfile $filename] == 0) } {
412 error "hosttest_run: testcase file $filename does not exist"
413 }
414
415 # There is no need to worry about interacting with the program,
416 # just exec it. It is a good idea to do this in the testcase directory,
417 # so that any core dumps get cleaned up as well.
418 set current_dir [pwd]
419 set status [ catch { cd [file join $::objdir "testcase"] } message ]
420 if { $status != 0 } {
421 error "unable to change directory to [file join $::objdir testcase]\n$message"
422 }
423
424 verbose -log -- $filename $args
425 set status [ catch { set result [eval exec -keepnewline -- $filename $args] } output]
426 if { $status == 0 } {
427 # The command has succeeded. The exit code is 0 and the output
428 # was returned by the exec.
429 set output $result
430 set result 0
431 } else {
432 # The command has failed. The exit code is 1 and the output is
433 # already in the right place.
434 verbose -log -- $output
435 set result 1
436 }
437 set status [catch { cd $current_dir } message]
438 if { $status != 0 } {
439 error "unable to change directory back to $current_dir"
440 }
441 }
442
443 # ----------------------------------------------------------------------------
444 # Given some test output, look through it for pass and fail messages.
445 # These should result in appropriate DejaGnu pass and fail calls.
446 # In addition, if the program exited with a non-zero status code but
447 # did not report any failures then a special failure is reported.
448
449 proc hosttest_handle_output { name result output } {
450
451 set passes 0
452 set fails 0
453
454 foreach line [split $output "\n"] {
455
456 # The output should be of one of the following forms:
457 # PASS:<message>
458 # FAIL:<message> Line: xx File: xx
459 # Whatever
460 #
461 # PASS and FAIL messages will be reported via DejaGnu pass and fail
462 # calls. Everything else gets passed to verbose, so the user gets
463 # to choose how much information gets reported.
464
465 set dummy ""
466 set match1 ""
467 set match2 ""
468
469 if { [regexp -- {^PASS:<(.*)>.*$} $line dummy match1] == 1 } {
470 pass $match1
471 incr passes
472 } elseif { [regexp -- {^FAIL:<(.*)>(.*)$} $line dummy match1 match2] == 1 } {
473 fail "$match1 $match2"
474 incr fails
475 } else {
476 verbose $line
477 }
478 }
479
480 if { ($result != 0) && ($fails == 0) } {
481 fail "program terminated with non-zero exit code but did not report any failures"
482 } elseif { ($passes == 0) && ($fails == 0) } {
483 unresolved "test case $name did not report any passes or failures"
484 }
485 }
486
487 # ----------------------------------------------------------------------------
488 # hosttest_run_test_with_filter
489 # This routines combines the compile, run and clean operations,
490 # plus it invokes a supplied callback to filter the output. The
491 # callback is passed three arguments: the test name, the exit code,
492 # and all of the program output.
493
494 proc hosttest_run_test_with_filter { name filter sources incdirs libdirs libs args } {
495
496 set result 0
497 set output ""
498
499 set status [ catch { hosttest_compile $name $sources $incdirs $libdirs $libs } message]
500 if { $status != 0 } {
501 fail "unable to compile test case $name, $message"
502 hosttest_clean
503 return
504 }
505 set status [ catch { hosttest_run result output $name $args } message]
506 if { $status != 0 } {
507 fail "unable to run test case $name, $message"
508 hosttest_clean
509 return
510 }
511 set status [ catch { $filter $name $result $output } message]
512 if { $status != 0 } {
513 fail "unable to parse output from test case $name"
514 hosttest_clean
515 return
516 }
517
518 hosttest_clean
519 }
520
521 # ----------------------------------------------------------------------------
522 # hosttest_run_simple_test
523 # This routine combines the compile, run, output, and clean operations.
524 # The arguments are the same as for compilation, plus an additional
525 # list for run-time parameters to the test case.
526
527 proc hosttest_run_simple_test { name sources incdirs libdirs libs args } {
528
529
530 set result 0
531 set output ""
532
533 set status [ catch { hosttest_compile $name $sources $incdirs $libdirs $libs } message]
534 if { $status != 0 } {
535 fail "unable to compile test case $name, $message"
536 hosttest_clean
537 return
538 }
539 set status [ catch { hosttest_run result output $name $args } message]
540 if { $status != 0 } {
541 fail "unable to run test case $name, $message"
542 hosttest_clean
543 return
544 }
545 set status [ catch { hosttest_handle_output $name $result $output } message]
546 if { $status != 0 } {
547 fail "unable to parse output from test case $name"
548 hosttest_clean
549 return
550 }
551
552 hosttest_clean
553 }
554
555 # ----------------------------------------------------------------------------
556 # Filename translation. A particular file has been created and must now
557 # be accessed from Tcl.
558 #
559 # Under Unix everything just works.
560 #
561 # Under Windows, well there is cygwin and there is the Windows world.
562 # A file may have come from either. cygtclsh80 and hence DejaGnu is not
563 # fully integrated with cygwin, for example it does not know about
564 # cygwin mount points. There are also complications because of
565 # volume-relative filenames.
566 #
567 # The code here tries a number of different ways of finding a file which
568 # matches the name. It is possible that the result is not actually what
569 # was intended, but hopefully this case will never arise.
570
571 proc hosttest_translate_existing_filename { name } {
572
573 if { $::tcl_platform(platform) == "unix" } {
574 # The file should exist. It is worth checking just in case.
575 if { [file exists $name] == 0 } {
576 return ""
577 } else {
578 return $name
579 }
580 }
581
582 if { $::tcl_platform(platform) != "windows" } {
583 perror "The testing framework does not know about platform $::tcl_platform(platform)"
584 return ""
585 }
586
587 # Always get rid of any backslashes, they just cause trouble
588 regsub -all -- {\\} $name {/} name
589
590 # If the name is already valid, great.
591 if { [file exists $name] } {
592 return $name
593 }
594
595 # OK, try to use cygwin's cygpath utility to convert it.
596 set status [catch "exec cygpath -w $name" message]
597 if { $status == 0 } {
598 set cygwin_name ""
599 regsub -all -- {\\} $message {/} cygwin_name
600 if { [file exists $cygwin_name] } {
601 return $cygwin_name
602 }
603 }
604
605 # Is the name volumerelative? If so work out the current volume
606 # from the current directory and prepend this.
607 if { [file pathtype $name] == "volumerelative" } {
608 append fullname [string range [pwd] 0 1] $name
609 if { [file exists $fullname] } {
610 return $fullname
611 }
612 }
613
614 # There are other possibilities, e.g. d:xxx indicating a file
615 # relative to the current directory on drive d:. For now such
616 # Lovecraftian abominations are ignored.
617 return ""
618 }
619
620 # ----------------------------------------------------------------------------
621 # Support for assertion dumps. The infrastructure allows other subsystems
622 # to add their own callbacks which get invoked during a panic and which
623 # can write additional output to the dump file. For example it would be
624 # possible to output full details of the current configuration. These
625 # routines make it easier to write test cases for such callbacks.
626 #
627 # hosttest_assert_check(result output)
628 # Make sure that the test case really triggered an assertion.
629 #
630 # hosttest_assert_read_dump(output)
631 # Identify the temporary file used for the dump, read it in, delete
632 # it (no point in leaving such temporaries lying around when running
633 # testcases) and return the contents of the file.
634 #
635 # hosttest_assert_extract_callback(dump title)
636 # Given a dump output as returned by read_dump, look for a section
637 # generated by a callback with the given title. Return the contents
638 # of the callback.
639
640 proc hosttest_assert_check { result output } {
641
642 if { $result == 0 } {
643 return 0
644 }
645
646 foreach line [split $output "\n"] {
647 if { [string match "Assertion failure*" $line] } {
648 return 1
649 }
650 }
651 return 0
652 }
653
654 # This routine assumes that assert_check has already been called.
655 proc hosttest_assert_read_dump { output } {
656
657 foreach line [split $output "\n"] {
658 set dummy ""
659 set match ""
660
661 if { [regexp -nocase -- {^writing additional output to (.*)$} $line dummy match] } {
662
663 # The filename is in match, but it may not be directly accessible.
664 set filename [hosttest_translate_existing_filename $match]
665 if { $filename == "" } {
666 return ""
667 }
668 set status [ catch {
669 set fd [open $filename r]
670 set data [read $fd]
671 close $fd
672 file delete $filename
673 } message]
674 if { $status != 0 } {
675 unresolved "Unable to process assertion dump file $filename"
676 unresolved "File $filename may have to be deleted manually"
677 return ""
678 }
679 return $data
680 }
681 }
682 return ""
683 }
684
685 # Look for the appropriate markers. Also clean up blank lines
686 # at the start and end.
687 proc hosttest_assert_extract_callback { dump title } {
688
689 set lines [split $dump "\n"]
690 set result ""
691
692 while { [llength $lines] > 0 } {
693 set line [lindex $lines 0]
694 set lines [lreplace $lines 0 0]
695
696 if { [regexp -nocase -- "^\# \{\{\{.*${title}.*\$" $line] } {
697
698 # Skip any blank lines at the start
699 while { [llength $lines] > 0 } {
700 set line [lindex $lines 0]
701 if { [regexp -- {^ *$} $line] == 0} {
702 break
703 }
704 set lines [lreplace $lines 0 0]
705 }
706
707 # Now add any lines until the close marker.
708 # Nested folds are not supported yet.
709 while { [llength $lines] > 0 } {
710 set line [lindex $lines 0]
711 set lines [lreplace $lines 0 0]
712 if { [regexp -nocase -- {^\# \}\}\}.*$} $line] } {
713 break
714 }
715 append result $line "\n"
716 }
717
718 return $result
719 }
720 }
721
722 return ""
723 }
724
725
726
727