1# Copyright 2008-2015 Nokia Networks 2# Copyright 2016- Robot Framework Foundation 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import fnmatch 17import glob 18import io 19import os 20import shutil 21import sys 22import tempfile 23import time 24 25from robot.version import get_version 26from robot.api import logger 27from robot.utils import (abspath, ConnectionCache, console_decode, del_env_var, 28 get_env_var, get_env_vars, get_time, is_truthy, 29 is_unicode, normpath, parse_time, plural_or_not, 30 secs_to_timestamp, secs_to_timestr, seq2str, 31 set_env_var, timestr_to_secs, unic, CONSOLE_ENCODING, 32 IRONPYTHON, JYTHON, PY2, PY3, SYSTEM_ENCODING, WINDOWS) 33 34__version__ = get_version() 35PROCESSES = ConnectionCache('No active processes.') 36 37 38class OperatingSystem(object): 39 """A test library providing keywords for OS related tasks. 40 41 ``OperatingSystem`` is Robot Framework's standard library that 42 enables various operating system related tasks to be performed in 43 the system where Robot Framework is running. It can, among other 44 things, execute commands (e.g. `Run`), create and remove files and 45 directories (e.g. `Create File`, `Remove Directory`), check 46 whether files or directories exists or contain something 47 (e.g. `File Should Exist`, `Directory Should Be Empty`) and 48 manipulate environment variables (e.g. `Set Environment Variable`). 49 50 == Table of contents == 51 52 - `Path separators` 53 - `Pattern matching` 54 - `Tilde expansion` 55 - `Boolean arguments` 56 - `Example` 57 - `Shortcuts` 58 - `Keywords` 59 60 = Path separators = 61 62 Because Robot Framework uses the backslash (``\\``) as an escape character 63 in the test data, using a literal backslash requires duplicating it like 64 in ``c:\\\\path\\\\file.txt``. That can be inconvenient especially with 65 longer Windows paths, and thus all keywords expecting paths as arguments 66 convert forward slashes to backslashes automatically on Windows. This also 67 means that paths like ``${CURDIR}/path/file.txt`` are operating system 68 independent. 69 70 Notice that the automatic path separator conversion does not work if 71 the path is only a part of an argument like with `Run` and `Start Process` 72 keywords. In these cases the built-in variable ``${/}`` that contains 73 ``\\`` or ``/``, depending on the operating system, can be used instead. 74 75 = Pattern matching = 76 77 Some keywords allow their arguments to be specified as 78 [http://en.wikipedia.org/wiki/Glob_(programming)|glob patterns] where: 79 80 | ``*`` | matches any string, even an empty string | 81 | ``?`` | matches any single character | 82 | ``[chars]`` | matches one character in the bracket | 83 | ``[!chars]`` | matches one character not in the bracket | 84 | ``[a-z]`` | matches one character from the range in the bracket | 85 | ``[!a-z]`` | matches one character not from the range in the bracket | 86 87 Unless otherwise noted, matching is case-insensitive on 88 case-insensitive operating systems such as Windows. 89 90 Starting from Robot Framework 2.9.1, globbing is not done if the given path 91 matches an existing file even if it would contain a glob pattern. 92 93 = Tilde expansion = 94 95 Paths beginning with ``~`` or ``~username`` are expanded to the current or 96 specified user's home directory, respectively. The resulting path is 97 operating system dependent, but typically e.g. ``~/robot`` is expanded to 98 ``C:\\Users\\<user>\\robot`` on Windows and ``/home/<user>/robot`` on 99 Unixes. 100 101 The ``~username`` form does not work on Jython. 102 103 = Boolean arguments = 104 105 Some keywords accept arguments that are handled as Boolean values true or 106 false. If such an argument is given as a string, it is considered false if 107 it is an empty string or equal to ``FALSE``, ``NONE``, ``NO``, ``OFF`` or 108 ``0``, case-insensitively. Other strings are considered true regardless 109 their value, and other argument types are tested using the same 110 [http://docs.python.org/library/stdtypes.html#truth|rules as in Python]. 111 112 True examples: 113 | `Remove Directory` | ${path} | recursive=True | # Strings are generally true. | 114 | `Remove Directory` | ${path} | recursive=yes | # Same as the above. | 115 | `Remove Directory` | ${path} | recursive=${TRUE} | # Python ``True`` is true. | 116 | `Remove Directory` | ${path} | recursive=${42} | # Numbers other than 0 are true. | 117 118 False examples: 119 | `Remove Directory` | ${path} | recursive=False | # String ``false`` is false. | 120 | `Remove Directory` | ${path} | recursive=no | # Also string ``no`` is false. | 121 | `Remove Directory` | ${path} | recursive=${EMPTY} | # Empty string is false. | 122 | `Remove Directory` | ${path} | recursive=${FALSE} | # Python ``False`` is false. | 123 124 Considering string ``NONE`` false is new in Robot Framework 3.0.3 and 125 considering also ``OFF`` and ``0`` false is new in Robot Framework 3.1. 126 127 = Example = 128 129 | =Setting= | =Value= | 130 | Library | OperatingSystem | 131 132 | =Variable= | =Value= | 133 | ${PATH} | ${CURDIR}/example.txt | 134 135 | =Test Case= | =Action= | =Argument= | =Argument= | 136 | Example | Create File | ${PATH} | Some text | 137 | | File Should Exist | ${PATH} | | 138 | | Copy File | ${PATH} | ~/file.txt | 139 | | ${output} = | Run | ${TEMPDIR}${/}script.py arg | 140 """ 141 ROBOT_LIBRARY_SCOPE = 'GLOBAL' 142 ROBOT_LIBRARY_VERSION = __version__ 143 144 def run(self, command): 145 """Runs the given command in the system and returns the output. 146 147 The execution status of the command *is not checked* by this 148 keyword, and it must be done separately based on the returned 149 output. If the execution return code is needed, either `Run 150 And Return RC` or `Run And Return RC And Output` can be used. 151 152 The standard error stream is automatically redirected to the standard 153 output stream by adding ``2>&1`` after the executed command. This 154 automatic redirection is done only when the executed command does not 155 contain additional output redirections. You can thus freely forward 156 the standard error somewhere else, for example, like 157 ``my_command 2>stderr.txt``. 158 159 The returned output contains everything written into the standard 160 output or error streams by the command (unless either of them 161 is redirected explicitly). Many commands add an extra newline 162 (``\\n``) after the output to make it easier to read in the 163 console. To ease processing the returned output, this possible 164 trailing newline is stripped by this keyword. 165 166 Examples: 167 | ${output} = | Run | ls -lhF /tmp | 168 | Log | ${output} | 169 | ${result} = | Run | ${CURDIR}${/}tester.py arg1 arg2 | 170 | Should Not Contain | ${result} | FAIL | 171 | ${stdout} = | Run | /opt/script.sh 2>/tmp/stderr.txt | 172 | Should Be Equal | ${stdout} | TEST PASSED | 173 | File Should Be Empty | /tmp/stderr.txt | 174 175 *TIP:* `Run Process` keyword provided by the 176 [http://robotframework.org/robotframework/latest/libraries/Process.html| 177 Process library] supports better process configuration and is generally 178 recommended as a replacement for this keyword. 179 """ 180 return self._run(command)[1] 181 182 def run_and_return_rc(self, command): 183 """Runs the given command in the system and returns the return code. 184 185 The return code (RC) is returned as a positive integer in 186 range from 0 to 255 as returned by the executed command. On 187 some operating systems (notable Windows) original return codes 188 can be something else, but this keyword always maps them to 189 the 0-255 range. Since the RC is an integer, it must be 190 checked e.g. with the keyword `Should Be Equal As Integers` 191 instead of `Should Be Equal` (both are built-in keywords). 192 193 Examples: 194 | ${rc} = | Run and Return RC | ${CURDIR}${/}script.py arg | 195 | Should Be Equal As Integers | ${rc} | 0 | 196 | ${rc} = | Run and Return RC | /path/to/example.rb arg1 arg2 | 197 | Should Be True | 0 < ${rc} < 42 | 198 199 See `Run` and `Run And Return RC And Output` if you need to get the 200 output of the executed command. 201 202 *TIP:* `Run Process` keyword provided by the 203 [http://robotframework.org/robotframework/latest/libraries/Process.html| 204 Process library] supports better process configuration and is generally 205 recommended as a replacement for this keyword. 206 """ 207 return self._run(command)[0] 208 209 def run_and_return_rc_and_output(self, command): 210 """Runs the given command in the system and returns the RC and output. 211 212 The return code (RC) is returned similarly as with `Run And Return RC` 213 and the output similarly as with `Run`. 214 215 Examples: 216 | ${rc} | ${output} = | Run and Return RC and Output | ${CURDIR}${/}mytool | 217 | Should Be Equal As Integers | ${rc} | 0 | 218 | Should Not Contain | ${output} | FAIL | 219 | ${rc} | ${stdout} = | Run and Return RC and Output | /opt/script.sh 2>/tmp/stderr.txt | 220 | Should Be True | ${rc} > 42 | 221 | Should Be Equal | ${stdout} | TEST PASSED | 222 | File Should Be Empty | /tmp/stderr.txt | 223 224 *TIP:* `Run Process` keyword provided by the 225 [http://robotframework.org/robotframework/latest/libraries/Process.html| 226 Process library] supports better process configuration and is generally 227 recommended as a replacement for this keyword. 228 """ 229 return self._run(command) 230 231 def _run(self, command): 232 process = _Process(command) 233 self._info("Running command '%s'." % process) 234 stdout = process.read() 235 rc = process.close() 236 return rc, stdout 237 238 def get_file(self, path, encoding='UTF-8', encoding_errors='strict'): 239 """Returns the contents of a specified file. 240 241 This keyword reads the specified file and returns the contents. 242 Line breaks in content are converted to platform independent form. 243 See also `Get Binary File`. 244 245 ``encoding`` defines the encoding of the file. The default value is 246 ``UTF-8``, which means that UTF-8 and ASCII encoded files are read 247 correctly. In addition to the encodings supported by the underlying 248 Python implementation, the following special encoding values can be 249 used: 250 251 - ``SYSTEM``: Use the default system encoding. 252 - ``CONSOLE``: Use the console encoding. Outside Windows this is same 253 as the system encoding. 254 255 ``encoding_errors`` argument controls what to do if decoding some bytes 256 fails. All values accepted by ``decode`` method in Python are valid, but 257 in practice the following values are most useful: 258 259 - ``strict``: Fail if characters cannot be decoded (default). 260 - ``ignore``: Ignore characters that cannot be decoded. 261 - ``replace``: Replace characters that cannot be decoded with 262 a replacement character. 263 264 Support for ``SYSTEM`` and ``CONSOLE`` encodings in Robot Framework 3.0. 265 """ 266 path = self._absnorm(path) 267 self._link("Getting file '%s'.", path) 268 encoding = self._map_encoding(encoding) 269 if IRONPYTHON: 270 # https://github.com/IronLanguages/main/issues/1233 271 with open(path) as f: 272 content = f.read().decode(encoding, encoding_errors) 273 else: 274 with io.open(path, encoding=encoding, errors=encoding_errors, 275 newline='') as f: 276 content = f.read() 277 return content.replace('\r\n', '\n') 278 279 def _map_encoding(self, encoding): 280 # Python 3 opens files in native system encoding by default. 281 if PY3 and encoding.upper() == 'SYSTEM': 282 return None 283 return {'SYSTEM': SYSTEM_ENCODING, 284 'CONSOLE': CONSOLE_ENCODING}.get(encoding.upper(), encoding) 285 286 def get_binary_file(self, path): 287 """Returns the contents of a specified file. 288 289 This keyword reads the specified file and returns the contents as is. 290 See also `Get File`. 291 """ 292 path = self._absnorm(path) 293 self._link("Getting file '%s'.", path) 294 with open(path, 'rb') as f: 295 return bytes(f.read()) 296 297 def grep_file(self, path, pattern, encoding='UTF-8', encoding_errors='strict'): 298 """Returns the lines of the specified file that match the ``pattern``. 299 300 This keyword reads a file from the file system using the defined 301 ``path``, ``encoding`` and ``encoding_errors`` similarly as `Get File`. 302 A difference is that only the lines that match the given ``pattern`` are 303 returned. Lines are returned as a single string catenated back together 304 with newlines and the number of matched lines is automatically logged. 305 Possible trailing newline is never returned. 306 307 A line matches if it contains the ``pattern`` anywhere in it and 308 it *does not need to match the pattern fully*. The pattern 309 matching syntax is explained in `introduction`, and in this 310 case matching is case-sensitive. 311 312 Examples: 313 | ${errors} = | Grep File | /var/log/myapp.log | ERROR | 314 | ${ret} = | Grep File | ${CURDIR}/file.txt | [Ww]ildc??d ex*ple | 315 316 If more complex pattern matching is needed, it is possible to use 317 `Get File` in combination with String library keywords like `Get 318 Lines Matching Regexp`. 319 """ 320 pattern = '*%s*' % pattern 321 path = self._absnorm(path) 322 lines = [] 323 total_lines = 0 324 self._link("Reading file '%s'.", path) 325 with io.open(path, encoding=encoding, errors=encoding_errors) as f: 326 for line in f.readlines(): 327 total_lines += 1 328 line = line.rstrip('\r\n') 329 if fnmatch.fnmatchcase(line, pattern): 330 lines.append(line) 331 self._info('%d out of %d lines matched' % (len(lines), total_lines)) 332 return '\n'.join(lines) 333 334 def log_file(self, path, encoding='UTF-8', encoding_errors='strict'): 335 """Wrapper for `Get File` that also logs the returned file. 336 337 The file is logged with the INFO level. If you want something else, 338 just use `Get File` and the built-in keyword `Log` with the desired 339 level. 340 341 See `Get File` for more information about ``encoding`` and 342 ``encoding_errors`` arguments. 343 """ 344 content = self.get_file(path, encoding, encoding_errors) 345 self._info(content) 346 return content 347 348 # File and directory existence 349 350 def should_exist(self, path, msg=None): 351 """Fails unless the given path (file or directory) exists. 352 353 The path can be given as an exact path or as a glob pattern. 354 The pattern matching syntax is explained in `introduction`. 355 The default error message can be overridden with the ``msg`` argument. 356 """ 357 path = self._absnorm(path) 358 if not self._glob(path): 359 self._fail(msg, "Path '%s' does not exist." % path) 360 self._link("Path '%s' exists.", path) 361 362 def should_not_exist(self, path, msg=None): 363 """Fails if the given path (file or directory) exists. 364 365 The path can be given as an exact path or as a glob pattern. 366 The pattern matching syntax is explained in `introduction`. 367 The default error message can be overridden with the ``msg`` argument. 368 """ 369 path = self._absnorm(path) 370 matches = self._glob(path) 371 if matches: 372 self._fail(msg, self._get_matches_error('Path', path, matches)) 373 self._link("Path '%s' does not exist.", path) 374 375 def _glob(self, path): 376 return glob.glob(path) if not os.path.exists(path) else [path] 377 378 def _get_matches_error(self, what, path, matches): 379 if not self._is_glob_path(path): 380 return "%s '%s' exists." % (what, path) 381 return "%s '%s' matches %s." % (what, path, seq2str(sorted(matches))) 382 383 def _is_glob_path(self, path): 384 return '*' in path or '?' in path or ('[' in path and ']' in path) 385 386 def file_should_exist(self, path, msg=None): 387 """Fails unless the given ``path`` points to an existing file. 388 389 The path can be given as an exact path or as a glob pattern. 390 The pattern matching syntax is explained in `introduction`. 391 The default error message can be overridden with the ``msg`` argument. 392 """ 393 path = self._absnorm(path) 394 matches = [p for p in self._glob(path) if os.path.isfile(p)] 395 if not matches: 396 self._fail(msg, "File '%s' does not exist." % path) 397 self._link("File '%s' exists.", path) 398 399 def file_should_not_exist(self, path, msg=None): 400 """Fails if the given path points to an existing file. 401 402 The path can be given as an exact path or as a glob pattern. 403 The pattern matching syntax is explained in `introduction`. 404 The default error message can be overridden with the ``msg`` argument. 405 """ 406 path = self._absnorm(path) 407 matches = [p for p in self._glob(path) if os.path.isfile(p)] 408 if matches: 409 self._fail(msg, self._get_matches_error('File', path, matches)) 410 self._link("File '%s' does not exist.", path) 411 412 def directory_should_exist(self, path, msg=None): 413 """Fails unless the given path points to an existing directory. 414 415 The path can be given as an exact path or as a glob pattern. 416 The pattern matching syntax is explained in `introduction`. 417 The default error message can be overridden with the ``msg`` argument. 418 """ 419 path = self._absnorm(path) 420 matches = [p for p in self._glob(path) if os.path.isdir(p)] 421 if not matches: 422 self._fail(msg, "Directory '%s' does not exist." % path) 423 self._link("Directory '%s' exists.", path) 424 425 def directory_should_not_exist(self, path, msg=None): 426 """Fails if the given path points to an existing file. 427 428 The path can be given as an exact path or as a glob pattern. 429 The pattern matching syntax is explained in `introduction`. 430 The default error message can be overridden with the ``msg`` argument. 431 """ 432 path = self._absnorm(path) 433 matches = [p for p in self._glob(path) if os.path.isdir(p)] 434 if matches: 435 self._fail(msg, self._get_matches_error('Directory', path, matches)) 436 self._link("Directory '%s' does not exist.", path) 437 438 # Waiting file/dir to appear/disappear 439 440 def wait_until_removed(self, path, timeout='1 minute'): 441 """Waits until the given file or directory is removed. 442 443 The path can be given as an exact path or as a glob pattern. 444 The pattern matching syntax is explained in `introduction`. 445 If the path is a pattern, the keyword waits until all matching 446 items are removed. 447 448 The optional ``timeout`` can be used to control the maximum time of 449 waiting. The timeout is given as a timeout string, e.g. in a format 450 ``15 seconds``, ``1min 10s`` or just ``10``. The time string format is 451 described in an appendix of Robot Framework User Guide. 452 453 If the timeout is negative, the keyword is never timed-out. The keyword 454 returns immediately, if the path does not exist in the first place. 455 """ 456 path = self._absnorm(path) 457 timeout = timestr_to_secs(timeout) 458 maxtime = time.time() + timeout 459 while self._glob(path): 460 if timeout >= 0 and time.time() > maxtime: 461 self._fail("'%s' was not removed in %s." 462 % (path, secs_to_timestr(timeout))) 463 time.sleep(0.1) 464 self._link("'%s' was removed.", path) 465 466 def wait_until_created(self, path, timeout='1 minute'): 467 """Waits until the given file or directory is created. 468 469 The path can be given as an exact path or as a glob pattern. 470 The pattern matching syntax is explained in `introduction`. 471 If the path is a pattern, the keyword returns when an item matching 472 it is created. 473 474 The optional ``timeout`` can be used to control the maximum time of 475 waiting. The timeout is given as a timeout string, e.g. in a format 476 ``15 seconds``, ``1min 10s`` or just ``10``. The time string format is 477 described in an appendix of Robot Framework User Guide. 478 479 If the timeout is negative, the keyword is never timed-out. The keyword 480 returns immediately, if the path already exists. 481 """ 482 path = self._absnorm(path) 483 timeout = timestr_to_secs(timeout) 484 maxtime = time.time() + timeout 485 while not self._glob(path): 486 if timeout >= 0 and time.time() > maxtime: 487 self._fail("'%s' was not created in %s." 488 % (path, secs_to_timestr(timeout))) 489 time.sleep(0.1) 490 self._link("'%s' was created.", path) 491 492 # Dir/file empty 493 494 def directory_should_be_empty(self, path, msg=None): 495 """Fails unless the specified directory is empty. 496 497 The default error message can be overridden with the ``msg`` argument. 498 """ 499 path = self._absnorm(path) 500 items = self._list_dir(path) 501 if items: 502 self._fail(msg, "Directory '%s' is not empty. Contents: %s." 503 % (path, seq2str(items, lastsep=', '))) 504 self._link("Directory '%s' is empty.", path) 505 506 def directory_should_not_be_empty(self, path, msg=None): 507 """Fails if the specified directory is empty. 508 509 The default error message can be overridden with the ``msg`` argument. 510 """ 511 path = self._absnorm(path) 512 items = self._list_dir(path) 513 if not items: 514 self._fail(msg, "Directory '%s' is empty." % path) 515 self._link("Directory '%%s' contains %d item%s." 516 % (len(items), plural_or_not(items)), path) 517 518 def file_should_be_empty(self, path, msg=None): 519 """Fails unless the specified file is empty. 520 521 The default error message can be overridden with the ``msg`` argument. 522 """ 523 path = self._absnorm(path) 524 if not os.path.isfile(path): 525 self._error("File '%s' does not exist." % path) 526 size = os.stat(path).st_size 527 if size > 0: 528 self._fail(msg, 529 "File '%s' is not empty. Size: %d bytes." % (path, size)) 530 self._link("File '%s' is empty.", path) 531 532 def file_should_not_be_empty(self, path, msg=None): 533 """Fails if the specified directory is empty. 534 535 The default error message can be overridden with the ``msg`` argument. 536 """ 537 path = self._absnorm(path) 538 if not os.path.isfile(path): 539 self._error("File '%s' does not exist." % path) 540 size = os.stat(path).st_size 541 if size == 0: 542 self._fail(msg, "File '%s' is empty." % path) 543 self._link("File '%%s' contains %d bytes." % size, path) 544 545 # Creating and removing files and directory 546 547 def create_file(self, path, content='', encoding='UTF-8'): 548 """Creates a file with the given content and encoding. 549 550 If the directory where the file is created does not exist, it is 551 automatically created along with possible missing intermediate 552 directories. Possible existing file is overwritten. 553 554 On Windows newline characters (``\\n``) in content are automatically 555 converted to Windows native newline sequence (``\\r\\n``). 556 557 See `Get File` for more information about possible ``encoding`` values, 558 including special values ``SYSTEM`` and ``CONSOLE``. 559 560 Examples: 561 | Create File | ${dir}/example.txt | Hello, world! | | 562 | Create File | ${path} | Hyv\\xe4 esimerkki | Latin-1 | 563 | Create File | /tmp/foo.txt | 3\\nlines\\nhere\\n | SYSTEM | 564 565 Use `Append To File` if you want to append to an existing file 566 and `Create Binary File` if you need to write bytes without encoding. 567 `File Should Not Exist` can be used to avoid overwriting existing 568 files. 569 570 The support for ``SYSTEM`` and ``CONSOLE`` encodings is new in Robot 571 Framework 3.0. Automatically converting ``\\n`` to ``\\r\\n`` on 572 Windows is new in Robot Framework 3.1. 573 """ 574 path = self._write_to_file(path, content, encoding) 575 self._link("Created file '%s'.", path) 576 577 def _write_to_file(self, path, content, encoding=None, mode='w'): 578 path = self._absnorm(path) 579 parent = os.path.dirname(path) 580 if not os.path.exists(parent): 581 os.makedirs(parent) 582 # io.open() only accepts Unicode, not byte-strings, in text mode. 583 # We expect possible byte-strings to be all ASCII. 584 if PY2 and isinstance(content, str) and 'b' not in mode: 585 content = unicode(content) 586 if encoding: 587 encoding = self._map_encoding(encoding) 588 with io.open(path, mode, encoding=encoding) as f: 589 f.write(content) 590 return path 591 592 def create_binary_file(self, path, content): 593 """Creates a binary file with the given content. 594 595 If content is given as a Unicode string, it is first converted to bytes 596 character by character. All characters with ordinal below 256 can be 597 used and are converted to bytes with same values. Using characters 598 with higher ordinal is an error. 599 600 Byte strings, and possible other types, are written to the file as is. 601 602 If the directory for the file does not exist, it is created, along 603 with missing intermediate directories. 604 605 Examples: 606 | Create Binary File | ${dir}/example.png | ${image content} | 607 | Create Binary File | ${path} | \\x01\\x00\\xe4\\x00 | 608 609 Use `Create File` if you want to create a text file using a certain 610 encoding. `File Should Not Exist` can be used to avoid overwriting 611 existing files. 612 """ 613 if is_unicode(content): 614 content = bytes(bytearray(ord(c) for c in content)) 615 path = self._write_to_file(path, content, mode='wb') 616 self._link("Created binary file '%s'.", path) 617 618 def append_to_file(self, path, content, encoding='UTF-8'): 619 """Appends the given content to the specified file. 620 621 If the file exists, the given text is written to its end. If the file 622 does not exist, it is created. 623 624 Other than not overwriting possible existing files, this keyword works 625 exactly like `Create File`. See its documentation for more details 626 about the usage. 627 628 Note that special encodings ``SYSTEM`` and ``CONSOLE`` only work 629 with this keyword starting from Robot Framework 3.1.2. 630 """ 631 path = self._write_to_file(path, content, encoding, mode='a') 632 self._link("Appended to file '%s'.", path) 633 634 def remove_file(self, path): 635 """Removes a file with the given path. 636 637 Passes if the file does not exist, but fails if the path does 638 not point to a regular file (e.g. it points to a directory). 639 640 The path can be given as an exact path or as a glob pattern. 641 The pattern matching syntax is explained in `introduction`. 642 If the path is a pattern, all files matching it are removed. 643 """ 644 path = self._absnorm(path) 645 matches = self._glob(path) 646 if not matches: 647 self._link("File '%s' does not exist.", path) 648 for match in matches: 649 if not os.path.isfile(match): 650 self._error("Path '%s' is not a file." % match) 651 os.remove(match) 652 self._link("Removed file '%s'.", match) 653 654 def remove_files(self, *paths): 655 """Uses `Remove File` to remove multiple files one-by-one. 656 657 Example: 658 | Remove Files | ${TEMPDIR}${/}foo.txt | ${TEMPDIR}${/}bar.txt | ${TEMPDIR}${/}zap.txt | 659 """ 660 for path in paths: 661 self.remove_file(path) 662 663 def empty_directory(self, path): 664 """Deletes all the content from the given directory. 665 666 Deletes both files and sub-directories, but the specified directory 667 itself if not removed. Use `Remove Directory` if you want to remove 668 the whole directory. 669 """ 670 path = self._absnorm(path) 671 for item in self._list_dir(path, absolute=True): 672 if os.path.isdir(item): 673 shutil.rmtree(item) 674 else: 675 os.remove(item) 676 self._link("Emptied directory '%s'.", path) 677 678 def create_directory(self, path): 679 """Creates the specified directory. 680 681 Also possible intermediate directories are created. Passes if the 682 directory already exists, but fails if the path exists and is not 683 a directory. 684 """ 685 path = self._absnorm(path) 686 if os.path.isdir(path): 687 self._link("Directory '%s' already exists.", path ) 688 elif os.path.exists(path): 689 self._error("Path '%s' is not a directory." % path) 690 else: 691 os.makedirs(path) 692 self._link("Created directory '%s'.", path) 693 694 def remove_directory(self, path, recursive=False): 695 """Removes the directory pointed to by the given ``path``. 696 697 If the second argument ``recursive`` is given a true value (see 698 `Boolean arguments`), the directory is removed recursively. Otherwise 699 removing fails if the directory is not empty. 700 701 If the directory pointed to by the ``path`` does not exist, the keyword 702 passes, but it fails, if the ``path`` points to a file. 703 """ 704 path = self._absnorm(path) 705 if not os.path.exists(path): 706 self._link("Directory '%s' does not exist.", path) 707 elif not os.path.isdir(path): 708 self._error("Path '%s' is not a directory." % path) 709 else: 710 if is_truthy(recursive): 711 shutil.rmtree(path) 712 else: 713 self.directory_should_be_empty( 714 path, "Directory '%s' is not empty." % path) 715 os.rmdir(path) 716 self._link("Removed directory '%s'.", path) 717 718 # Moving and copying files and directories 719 720 def copy_file(self, source, destination): 721 """Copies the source file into the destination. 722 723 Source must be a path to an existing file or a glob pattern (see 724 `Pattern matching`) that matches exactly one file. How the 725 destination is interpreted is explained below. 726 727 1) If the destination is an existing file, the source file is copied 728 over it. 729 730 2) If the destination is an existing directory, the source file is 731 copied into it. A possible file with the same name as the source is 732 overwritten. 733 734 3) If the destination does not exist and it ends with a path 735 separator (``/`` or ``\\``), it is considered a directory. That 736 directory is created and a source file copied into it. 737 Possible missing intermediate directories are also created. 738 739 4) If the destination does not exist and it does not end with a path 740 separator, it is considered a file. If the path to the file does not 741 exist, it is created. 742 743 The resulting destination path is returned since Robot Framework 2.9.2. 744 745 See also `Copy Files`, `Move File`, and `Move Files`. 746 """ 747 source, destination = \ 748 self._prepare_copy_and_move_file(source, destination) 749 if not self._are_source_and_destination_same_file(source, destination): 750 source, destination = self._atomic_copy(source, destination) 751 self._link("Copied file from '%s' to '%s'.", source, destination) 752 return destination 753 754 def _prepare_copy_and_move_file(self, source, destination): 755 source = self._normalize_copy_and_move_source(source) 756 destination = self._normalize_copy_and_move_destination(destination) 757 if os.path.isdir(destination): 758 destination = os.path.join(destination, os.path.basename(source)) 759 return source, destination 760 761 def _normalize_copy_and_move_source(self, source): 762 source = self._absnorm(source) 763 sources = self._glob(source) 764 if len(sources) > 1: 765 self._error("Multiple matches with source pattern '%s'." % source) 766 if sources: 767 source = sources[0] 768 if not os.path.exists(source): 769 self._error("Source file '%s' does not exist." % source) 770 if not os.path.isfile(source): 771 self._error("Source file '%s' is not a regular file." % source) 772 return source 773 774 def _normalize_copy_and_move_destination(self, destination): 775 is_dir = os.path.isdir(destination) or destination.endswith(('/', '\\')) 776 destination = self._absnorm(destination) 777 directory = destination if is_dir else os.path.dirname(destination) 778 self._ensure_destination_directory_exists(directory) 779 return destination 780 781 def _ensure_destination_directory_exists(self, path): 782 if not os.path.exists(path): 783 os.makedirs(path) 784 elif not os.path.isdir(path): 785 self._error("Destination '%s' exists and is not a directory." % path) 786 787 def _are_source_and_destination_same_file(self, source, destination): 788 if self._force_normalize(source) == self._force_normalize(destination): 789 self._link("Source '%s' and destination '%s' point to the same " 790 "file.", source, destination) 791 return True 792 return False 793 794 def _force_normalize(self, path): 795 # TODO: Should normalize_path also support link normalization? 796 # TODO: Should we handle dos paths like 'exampl~1.txt'? 797 return os.path.realpath(normpath(path, case_normalize=True)) 798 799 def _atomic_copy(self, source, destination): 800 """Copy file atomically (or at least try to). 801 802 This method tries to ensure that a file copy operation will not fail 803 if the destination file is removed during copy operation. The problem 804 is that copying a file is typically not an atomic operation. 805 806 Luckily moving files is atomic in almost every platform, assuming files 807 are on the same filesystem, and we can use that as a workaround: 808 - First move the source to a temporary directory that is ensured to 809 be on the same filesystem as the destination. 810 - Move the temporary file over the real destination. 811 812 See also https://github.com/robotframework/robotframework/issues/1502 813 """ 814 temp_directory = tempfile.mkdtemp(dir=os.path.dirname(destination)) 815 temp_file = os.path.join(temp_directory, os.path.basename(source)) 816 try: 817 shutil.copy(source, temp_file) 818 if os.path.exists(destination): 819 os.remove(destination) 820 shutil.move(temp_file, destination) 821 finally: 822 shutil.rmtree(temp_directory) 823 return source, destination 824 825 def move_file(self, source, destination): 826 """Moves the source file into the destination. 827 828 Arguments have exactly same semantics as with `Copy File` keyword. 829 Destination file path is returned since Robot Framework 2.9.2. 830 831 If the source and destination are on the same filesystem, rename 832 operation is used. Otherwise file is copied to the destination 833 filesystem and then removed from the original filesystem. 834 835 See also `Move Files`, `Copy File`, and `Copy Files`. 836 """ 837 source, destination = \ 838 self._prepare_copy_and_move_file(source, destination) 839 if not self._are_source_and_destination_same_file(destination, source): 840 shutil.move(source, destination) 841 self._link("Moved file from '%s' to '%s'.", source, destination) 842 return destination 843 844 def copy_files(self, *sources_and_destination): 845 """Copies specified files to the target directory. 846 847 Source files can be given as exact paths and as glob patterns (see 848 `Pattern matching`). At least one source must be given, but it is 849 not an error if it is a pattern that does not match anything. 850 851 Last argument must be the destination directory. If the destination 852 does not exist, it will be created. 853 854 Examples: 855 | Copy Files | ${dir}/file1.txt | ${dir}/file2.txt | ${dir2} | 856 | Copy Files | ${dir}/file-*.txt | ${dir2} | | 857 858 See also `Copy File`, `Move File`, and `Move Files`. 859 """ 860 sources, destination \ 861 = self._prepare_copy_and_move_files(sources_and_destination) 862 for source in sources: 863 self.copy_file(source, destination) 864 865 def _prepare_copy_and_move_files(self, items): 866 if len(items) < 2: 867 self._error('Must contain destination and at least one source.') 868 sources = self._glob_files(items[:-1]) 869 destination = self._absnorm(items[-1]) 870 self._ensure_destination_directory_exists(destination) 871 return sources, destination 872 873 def _glob_files(self, patterns): 874 files = [] 875 for pattern in patterns: 876 files.extend(self._glob(self._absnorm(pattern))) 877 return files 878 879 def move_files(self, *sources_and_destination): 880 """Moves specified files to the target directory. 881 882 Arguments have exactly same semantics as with `Copy Files` keyword. 883 884 See also `Move File`, `Copy File`, and `Copy Files`. 885 """ 886 sources, destination \ 887 = self._prepare_copy_and_move_files(sources_and_destination) 888 for source in sources: 889 self.move_file(source, destination) 890 891 def copy_directory(self, source, destination): 892 """Copies the source directory into the destination. 893 894 If the destination exists, the source is copied under it. Otherwise 895 the destination directory and the possible missing intermediate 896 directories are created. 897 """ 898 source, destination \ 899 = self._prepare_copy_and_move_directory(source, destination) 900 try: 901 shutil.copytree(source, destination) 902 except shutil.Error: 903 # https://github.com/robotframework/robotframework/issues/2321 904 if not (WINDOWS and JYTHON): 905 raise 906 self._link("Copied directory from '%s' to '%s'.", source, destination) 907 908 def _prepare_copy_and_move_directory(self, source, destination): 909 source = self._absnorm(source) 910 destination = self._absnorm(destination) 911 if not os.path.exists(source): 912 self._error("Source '%s' does not exist." % source) 913 if not os.path.isdir(source): 914 self._error("Source '%s' is not a directory." % source) 915 if os.path.exists(destination) and not os.path.isdir(destination): 916 self._error("Destination '%s' is not a directory." % destination) 917 if os.path.exists(destination): 918 base = os.path.basename(source) 919 destination = os.path.join(destination, base) 920 else: 921 parent = os.path.dirname(destination) 922 if not os.path.exists(parent): 923 os.makedirs(parent) 924 return source, destination 925 926 def move_directory(self, source, destination): 927 """Moves the source directory into a destination. 928 929 Uses `Copy Directory` keyword internally, and ``source`` and 930 ``destination`` arguments have exactly same semantics as with 931 that keyword. 932 """ 933 source, destination \ 934 = self._prepare_copy_and_move_directory(source, destination) 935 shutil.move(source, destination) 936 self._link("Moved directory from '%s' to '%s'.", source, destination) 937 938 # Environment Variables 939 940 def get_environment_variable(self, name, default=None): 941 """Returns the value of an environment variable with the given name. 942 943 If no such environment variable is set, returns the default value, if 944 given. Otherwise fails the test case. 945 946 Returned variables are automatically decoded to Unicode using 947 the system encoding. 948 949 Note that you can also access environment variables directly using 950 the variable syntax ``%{ENV_VAR_NAME}``. 951 """ 952 value = get_env_var(name, default) 953 if value is None: 954 self._error("Environment variable '%s' does not exist." % name) 955 return value 956 957 def set_environment_variable(self, name, value): 958 """Sets an environment variable to a specified value. 959 960 Values are converted to strings automatically. Set variables are 961 automatically encoded using the system encoding. 962 """ 963 set_env_var(name, value) 964 self._info("Environment variable '%s' set to value '%s'." 965 % (name, value)) 966 967 def append_to_environment_variable(self, name, *values, **config): 968 """Appends given ``values`` to environment variable ``name``. 969 970 If the environment variable already exists, values are added after it, 971 and otherwise a new environment variable is created. 972 973 Values are, by default, joined together using the operating system 974 path separator (``;`` on Windows, ``:`` elsewhere). This can be changed 975 by giving a separator after the values like ``separator=value``. No 976 other configuration parameters are accepted. 977 978 Examples (assuming ``NAME`` and ``NAME2`` do not exist initially): 979 | Append To Environment Variable | NAME | first | | 980 | Should Be Equal | %{NAME} | first | | 981 | Append To Environment Variable | NAME | second | third | 982 | Should Be Equal | %{NAME} | first${:}second${:}third | 983 | Append To Environment Variable | NAME2 | first | separator=- | 984 | Should Be Equal | %{NAME2} | first | | 985 | Append To Environment Variable | NAME2 | second | separator=- | 986 | Should Be Equal | %{NAME2} | first-second | 987 """ 988 sentinel = object() 989 initial = self.get_environment_variable(name, sentinel) 990 if initial is not sentinel: 991 values = (initial,) + values 992 separator = config.pop('separator', os.pathsep) 993 if config: 994 config = ['='.join(i) for i in sorted(config.items())] 995 self._error('Configuration %s not accepted.' 996 % seq2str(config, lastsep=' or ')) 997 self.set_environment_variable(name, separator.join(values)) 998 999 def remove_environment_variable(self, *names): 1000 """Deletes the specified environment variable. 1001 1002 Does nothing if the environment variable is not set. 1003 1004 It is possible to remove multiple variables by passing them to this 1005 keyword as separate arguments. 1006 """ 1007 for name in names: 1008 value = del_env_var(name) 1009 if value: 1010 self._info("Environment variable '%s' deleted." % name) 1011 else: 1012 self._info("Environment variable '%s' does not exist." % name) 1013 1014 def environment_variable_should_be_set(self, name, msg=None): 1015 """Fails if the specified environment variable is not set. 1016 1017 The default error message can be overridden with the ``msg`` argument. 1018 """ 1019 value = get_env_var(name) 1020 if not value: 1021 self._fail(msg, "Environment variable '%s' is not set." % name) 1022 self._info("Environment variable '%s' is set to '%s'." % (name, value)) 1023 1024 def environment_variable_should_not_be_set(self, name, msg=None): 1025 """Fails if the specified environment variable is set. 1026 1027 The default error message can be overridden with the ``msg`` argument. 1028 """ 1029 value = get_env_var(name) 1030 if value: 1031 self._fail(msg, "Environment variable '%s' is set to '%s'." 1032 % (name, value)) 1033 self._info("Environment variable '%s' is not set." % name) 1034 1035 def get_environment_variables(self): 1036 """Returns currently available environment variables as a dictionary. 1037 1038 Both keys and values are decoded to Unicode using the system encoding. 1039 Altering the returned dictionary has no effect on the actual environment 1040 variables. 1041 """ 1042 return get_env_vars() 1043 1044 def log_environment_variables(self, level='INFO'): 1045 """Logs all environment variables using the given log level. 1046 1047 Environment variables are also returned the same way as with 1048 `Get Environment Variables` keyword. 1049 """ 1050 variables = get_env_vars() 1051 for name in sorted(variables, key=lambda item: item.lower()): 1052 self._log('%s = %s' % (name, variables[name]), level) 1053 return variables 1054 1055 # Path 1056 1057 def join_path(self, base, *parts): 1058 """Joins the given path part(s) to the given base path. 1059 1060 The path separator (``/`` or ``\\``) is inserted when needed and 1061 the possible absolute paths handled as expected. The resulted 1062 path is also normalized. 1063 1064 Examples: 1065 | ${path} = | Join Path | my | path | 1066 | ${p2} = | Join Path | my/ | path/ | 1067 | ${p3} = | Join Path | my | path | my | file.txt | 1068 | ${p4} = | Join Path | my | /path | 1069 | ${p5} = | Join Path | /my/path/ | .. | path2 | 1070 => 1071 - ${path} = 'my/path' 1072 - ${p2} = 'my/path' 1073 - ${p3} = 'my/path/my/file.txt' 1074 - ${p4} = '/path' 1075 - ${p5} = '/my/path2' 1076 """ 1077 base = base.replace('/', os.sep) 1078 parts = [p.replace('/', os.sep) for p in parts] 1079 return self.normalize_path(os.path.join(base, *parts)) 1080 1081 def join_paths(self, base, *paths): 1082 """Joins given paths with base and returns resulted paths. 1083 1084 See `Join Path` for more information. 1085 1086 Examples: 1087 | @{p1} = | Join Paths | base | example | other | | 1088 | @{p2} = | Join Paths | /my/base | /example | other | | 1089 | @{p3} = | Join Paths | my/base | example/path/ | other | one/more | 1090 => 1091 - @{p1} = ['base/example', 'base/other'] 1092 - @{p2} = ['/example', '/my/base/other'] 1093 - @{p3} = ['my/base/example/path', 'my/base/other', 'my/base/one/more'] 1094 """ 1095 return [self.join_path(base, path) for path in paths] 1096 1097 def normalize_path(self, path, case_normalize=False): 1098 """Normalizes the given path. 1099 1100 - Collapses redundant separators and up-level references. 1101 - Converts ``/`` to ``\\`` on Windows. 1102 - Replaces initial ``~`` or ``~user`` by that user's home directory. 1103 The latter is not supported on Jython. 1104 - If ``case_normalize`` is given a true value (see `Boolean arguments`) 1105 on Windows, converts the path to all lowercase. New in Robot 1106 Framework 3.1. 1107 1108 Examples: 1109 | ${path1} = | Normalize Path | abc/ | 1110 | ${path2} = | Normalize Path | abc/../def | 1111 | ${path3} = | Normalize Path | abc/./def//ghi | 1112 | ${path4} = | Normalize Path | ~robot/stuff | 1113 => 1114 - ${path1} = 'abc' 1115 - ${path2} = 'def' 1116 - ${path3} = 'abc/def/ghi' 1117 - ${path4} = '/home/robot/stuff' 1118 1119 On Windows result would use ``\\`` instead of ``/`` and home directory 1120 would be different. 1121 """ 1122 path = os.path.normpath(os.path.expanduser(path.replace('/', os.sep))) 1123 # os.path.normcase doesn't normalize on OSX which also, by default, 1124 # has case-insensitive file system. Our robot.utils.normpath would 1125 # do that, but it's not certain would that, or other things that the 1126 # utility do, desirable. 1127 if case_normalize: 1128 path = os.path.normcase(path) 1129 return path or '.' 1130 1131 def split_path(self, path): 1132 """Splits the given path from the last path separator (``/`` or ``\\``). 1133 1134 The given path is first normalized (e.g. a possible trailing 1135 path separator is removed, special directories ``..`` and ``.`` 1136 removed). The parts that are split are returned as separate 1137 components. 1138 1139 Examples: 1140 | ${path1} | ${dir} = | Split Path | abc/def | 1141 | ${path2} | ${file} = | Split Path | abc/def/ghi.txt | 1142 | ${path3} | ${d2} = | Split Path | abc/../def/ghi/ | 1143 => 1144 - ${path1} = 'abc' & ${dir} = 'def' 1145 - ${path2} = 'abc/def' & ${file} = 'ghi.txt' 1146 - ${path3} = 'def' & ${d2} = 'ghi' 1147 """ 1148 return os.path.split(self.normalize_path(path)) 1149 1150 def split_extension(self, path): 1151 """Splits the extension from the given path. 1152 1153 The given path is first normalized (e.g. possible trailing 1154 path separators removed, special directories ``..`` and ``.`` 1155 removed). The base path and extension are returned as separate 1156 components so that the dot used as an extension separator is 1157 removed. If the path contains no extension, an empty string is 1158 returned for it. Possible leading and trailing dots in the file 1159 name are never considered to be extension separators. 1160 1161 Examples: 1162 | ${path} | ${ext} = | Split Extension | file.extension | 1163 | ${p2} | ${e2} = | Split Extension | path/file.ext | 1164 | ${p3} | ${e3} = | Split Extension | path/file | 1165 | ${p4} | ${e4} = | Split Extension | p1/../p2/file.ext | 1166 | ${p5} | ${e5} = | Split Extension | path/.file.ext | 1167 | ${p6} | ${e6} = | Split Extension | path/.file | 1168 => 1169 - ${path} = 'file' & ${ext} = 'extension' 1170 - ${p2} = 'path/file' & ${e2} = 'ext' 1171 - ${p3} = 'path/file' & ${e3} = '' 1172 - ${p4} = 'p2/file' & ${e4} = 'ext' 1173 - ${p5} = 'path/.file' & ${e5} = 'ext' 1174 - ${p6} = 'path/.file' & ${e6} = '' 1175 """ 1176 path = self.normalize_path(path) 1177 basename = os.path.basename(path) 1178 if basename.startswith('.' * basename.count('.')): 1179 return path, '' 1180 if path.endswith('.'): 1181 path2 = path.rstrip('.') 1182 trailing_dots = '.' * (len(path) - len(path2)) 1183 path = path2 1184 else: 1185 trailing_dots = '' 1186 basepath, extension = os.path.splitext(path) 1187 if extension.startswith('.'): 1188 extension = extension[1:] 1189 if extension: 1190 extension += trailing_dots 1191 else: 1192 basepath += trailing_dots 1193 return basepath, extension 1194 1195 # Misc 1196 1197 def get_modified_time(self, path, format='timestamp'): 1198 """Returns the last modification time of a file or directory. 1199 1200 How time is returned is determined based on the given ``format`` 1201 string as follows. Note that all checks are case-insensitive. 1202 Returned time is also automatically logged. 1203 1204 1) If ``format`` contains the word ``epoch``, the time is returned 1205 in seconds after the UNIX epoch. The return value is always 1206 an integer. 1207 1208 2) If ``format`` contains any of the words ``year``, ``month``, 1209 ``day``, ``hour``, ``min`` or ``sec``, only the selected parts are 1210 returned. The order of the returned parts is always the one 1211 in the previous sentence and the order of the words in 1212 ``format`` is not significant. The parts are returned as 1213 zero-padded strings (e.g. May -> ``05``). 1214 1215 3) Otherwise, and by default, the time is returned as a 1216 timestamp string in the format ``2006-02-24 15:08:31``. 1217 1218 Examples (when the modified time of ``${CURDIR}`` is 1219 2006-03-29 15:06:21): 1220 | ${time} = | Get Modified Time | ${CURDIR} | 1221 | ${secs} = | Get Modified Time | ${CURDIR} | epoch | 1222 | ${year} = | Get Modified Time | ${CURDIR} | return year | 1223 | ${y} | ${d} = | Get Modified Time | ${CURDIR} | year,day | 1224 | @{time} = | Get Modified Time | ${CURDIR} | year,month,day,hour,min,sec | 1225 => 1226 - ${time} = '2006-03-29 15:06:21' 1227 - ${secs} = 1143637581 1228 - ${year} = '2006' 1229 - ${y} = '2006' & ${d} = '29' 1230 - @{time} = ['2006', '03', '29', '15', '06', '21'] 1231 """ 1232 path = self._absnorm(path) 1233 if not os.path.exists(path): 1234 self._error("Path '%s' does not exist." % path) 1235 mtime = get_time(format, os.stat(path).st_mtime) 1236 self._link("Last modified time of '%%s' is %s." % mtime, path) 1237 return mtime 1238 1239 def set_modified_time(self, path, mtime): 1240 """Sets the file modification and access times. 1241 1242 Changes the modification and access times of the given file to 1243 the value determined by ``mtime``. The time can be given in 1244 different formats described below. Note that all checks 1245 involving strings are case-insensitive. Modified time can only 1246 be set to regular files. 1247 1248 1) If ``mtime`` is a number, or a string that can be converted 1249 to a number, it is interpreted as seconds since the UNIX 1250 epoch (1970-01-01 00:00:00 UTC). This documentation was 1251 originally written about 1177654467 seconds after the epoch. 1252 1253 2) If ``mtime`` is a timestamp, that time will be used. Valid 1254 timestamp formats are ``YYYY-MM-DD hh:mm:ss`` and 1255 ``YYYYMMDD hhmmss``. 1256 1257 3) If ``mtime`` is equal to ``NOW``, the current local time is used. 1258 1259 4) If ``mtime`` is equal to ``UTC``, the current time in 1260 [http://en.wikipedia.org/wiki/Coordinated_Universal_Time|UTC] 1261 is used. 1262 1263 5) If ``mtime`` is in the format like ``NOW - 1 day`` or ``UTC + 1 1264 hour 30 min``, the current local/UTC time plus/minus the time 1265 specified with the time string is used. The time string format 1266 is described in an appendix of Robot Framework User Guide. 1267 1268 Examples: 1269 | Set Modified Time | /path/file | 1177654467 | # Time given as epoch seconds | 1270 | Set Modified Time | /path/file | 2007-04-27 9:14:27 | # Time given as a timestamp | 1271 | Set Modified Time | /path/file | NOW | # The local time of execution | 1272 | Set Modified Time | /path/file | NOW - 1 day | # 1 day subtracted from the local time | 1273 | Set Modified Time | /path/file | UTC + 1h 2min 3s | # 1h 2min 3s added to the UTC time | 1274 """ 1275 mtime = parse_time(mtime) 1276 path = self._absnorm(path) 1277 if not os.path.exists(path): 1278 self._error("File '%s' does not exist." % path) 1279 if not os.path.isfile(path): 1280 self._error("Path '%s' is not a regular file." % path) 1281 os.utime(path, (mtime, mtime)) 1282 time.sleep(0.1) # Give os some time to really set these times 1283 tstamp = secs_to_timestamp(mtime, seps=('-', ' ', ':')) 1284 self._link("Set modified time of '%%s' to %s." % tstamp, path) 1285 1286 def get_file_size(self, path): 1287 """Returns and logs file size as an integer in bytes.""" 1288 path = self._absnorm(path) 1289 if not os.path.isfile(path): 1290 self._error("File '%s' does not exist." % path) 1291 size = os.stat(path).st_size 1292 plural = plural_or_not(size) 1293 self._link("Size of file '%%s' is %d byte%s." % (size, plural), path) 1294 return size 1295 1296 def list_directory(self, path, pattern=None, absolute=False): 1297 """Returns and logs items in a directory, optionally filtered with ``pattern``. 1298 1299 File and directory names are returned in case-sensitive alphabetical 1300 order, e.g. ``['A Name', 'Second', 'a lower case name', 'one more']``. 1301 Implicit directories ``.`` and ``..`` are not returned. The returned 1302 items are automatically logged. 1303 1304 File and directory names are returned relative to the given path 1305 (e.g. ``'file.txt'``) by default. If you want them be returned in 1306 absolute format (e.g. ``'/home/robot/file.txt'``), give the ``absolute`` 1307 argument a true value (see `Boolean arguments`). 1308 1309 If ``pattern`` is given, only items matching it are returned. The pattern 1310 matching syntax is explained in `introduction`, and in this case 1311 matching is case-sensitive. 1312 1313 Examples (using also other `List Directory` variants): 1314 | @{items} = | List Directory | ${TEMPDIR} | 1315 | @{files} = | List Files In Directory | /tmp | *.txt | absolute | 1316 | ${count} = | Count Files In Directory | ${CURDIR} | ??? | 1317 """ 1318 items = self._list_dir(path, pattern, absolute) 1319 self._info('%d item%s:\n%s' % (len(items), plural_or_not(items), 1320 '\n'.join(items))) 1321 return items 1322 1323 def list_files_in_directory(self, path, pattern=None, absolute=False): 1324 """Wrapper for `List Directory` that returns only files.""" 1325 files = self._list_files_in_dir(path, pattern, absolute) 1326 self._info('%d file%s:\n%s' % (len(files), plural_or_not(files), 1327 '\n'.join(files))) 1328 return files 1329 1330 def list_directories_in_directory(self, path, pattern=None, absolute=False): 1331 """Wrapper for `List Directory` that returns only directories.""" 1332 dirs = self._list_dirs_in_dir(path, pattern, absolute) 1333 self._info('%d director%s:\n%s' % (len(dirs), 1334 'y' if len(dirs) == 1 else 'ies', 1335 '\n'.join(dirs))) 1336 return dirs 1337 1338 def count_items_in_directory(self, path, pattern=None): 1339 """Returns and logs the number of all items in the given directory. 1340 1341 The argument ``pattern`` has the same semantics as with `List Directory` 1342 keyword. The count is returned as an integer, so it must be checked e.g. 1343 with the built-in keyword `Should Be Equal As Integers`. 1344 """ 1345 count = len(self._list_dir(path, pattern)) 1346 self._info("%s item%s." % (count, plural_or_not(count))) 1347 return count 1348 1349 def count_files_in_directory(self, path, pattern=None): 1350 """Wrapper for `Count Items In Directory` returning only file count.""" 1351 count = len(self._list_files_in_dir(path, pattern)) 1352 self._info("%s file%s." % (count, plural_or_not(count))) 1353 return count 1354 1355 def count_directories_in_directory(self, path, pattern=None): 1356 """Wrapper for `Count Items In Directory` returning only directory count.""" 1357 count = len(self._list_dirs_in_dir(path, pattern)) 1358 self._info("%s director%s." % (count, 'y' if count == 1 else 'ies')) 1359 return count 1360 1361 def _list_dir(self, path, pattern=None, absolute=False): 1362 path = self._absnorm(path) 1363 self._link("Listing contents of directory '%s'.", path) 1364 if not os.path.isdir(path): 1365 self._error("Directory '%s' does not exist." % path) 1366 # result is already unicode but unic also handles NFC normalization 1367 items = sorted(unic(item) for item in os.listdir(path)) 1368 if pattern: 1369 items = [i for i in items if fnmatch.fnmatchcase(i, pattern)] 1370 if is_truthy(absolute): 1371 path = os.path.normpath(path) 1372 items = [os.path.join(path, item) for item in items] 1373 return items 1374 1375 def _list_files_in_dir(self, path, pattern=None, absolute=False): 1376 return [item for item in self._list_dir(path, pattern, absolute) 1377 if os.path.isfile(os.path.join(path, item))] 1378 1379 def _list_dirs_in_dir(self, path, pattern=None, absolute=False): 1380 return [item for item in self._list_dir(path, pattern, absolute) 1381 if os.path.isdir(os.path.join(path, item))] 1382 1383 def touch(self, path): 1384 """Emulates the UNIX touch command. 1385 1386 Creates a file, if it does not exist. Otherwise changes its access and 1387 modification times to the current time. 1388 1389 Fails if used with the directories or the parent directory of the given 1390 file does not exist. 1391 """ 1392 path = self._absnorm(path) 1393 if os.path.isdir(path): 1394 self._error("Cannot touch '%s' because it is a directory." % path) 1395 if not os.path.exists(os.path.dirname(path)): 1396 self._error("Cannot touch '%s' because its parent directory does " 1397 "not exist." % path) 1398 if os.path.exists(path): 1399 mtime = round(time.time()) 1400 os.utime(path, (mtime, mtime)) 1401 self._link("Touched existing file '%s'.", path) 1402 else: 1403 open(path, 'w').close() 1404 self._link("Touched new file '%s'.", path) 1405 1406 def _absnorm(self, path): 1407 path = self.normalize_path(path) 1408 try: 1409 return abspath(path) 1410 except ValueError: # http://ironpython.codeplex.com/workitem/29489 1411 return path 1412 1413 def _fail(self, *messages): 1414 raise AssertionError(next(msg for msg in messages if msg)) 1415 1416 def _error(self, msg): 1417 raise RuntimeError(msg) 1418 1419 def _info(self, msg): 1420 self._log(msg, 'INFO') 1421 1422 def _link(self, msg, *paths): 1423 paths = tuple('<a href="file://%s">%s</a>' % (p, p) for p in paths) 1424 self._log(msg % paths, 'HTML') 1425 1426 def _warn(self, msg): 1427 self._log(msg, 'WARN') 1428 1429 def _log(self, msg, level): 1430 logger.write(msg, level) 1431 1432 1433class _Process: 1434 1435 def __init__(self, command): 1436 self._command = self._process_command(command) 1437 self._process = os.popen(self._command) 1438 1439 def __str__(self): 1440 return self._command 1441 1442 def read(self): 1443 return self._process_output(self._process.read()) 1444 1445 def close(self): 1446 try: 1447 rc = self._process.close() 1448 except IOError: # Has occurred sometimes in Windows 1449 return 255 1450 if rc is None: 1451 return 0 1452 # In Windows (Python and Jython) return code is value returned by 1453 # command (can be almost anything) 1454 # In other OS: 1455 # In Jython return code can be between '-255' - '255' 1456 # In Python return code must be converted with 'rc >> 8' and it is 1457 # between 0-255 after conversion 1458 if WINDOWS or JYTHON: 1459 return rc % 256 1460 return rc >> 8 1461 1462 def _process_command(self, command): 1463 if '>' not in command: 1464 if command.endswith('&'): 1465 command = command[:-1] + ' 2>&1 &' 1466 else: 1467 command += ' 2>&1' 1468 return self._encode_to_file_system(command) 1469 1470 def _encode_to_file_system(self, string): 1471 enc = sys.getfilesystemencoding() if PY2 else None 1472 return string.encode(enc) if enc else string 1473 1474 def _process_output(self, output): 1475 if '\r\n' in output: 1476 output = output.replace('\r\n', '\n') 1477 if output.endswith('\n'): 1478 output = output[:-1] 1479 return console_decode(output, force=True) 1480