Bug 784841 - Part 6: Move some functions from ConfigStatus to mozbuild; r=ted

--HG--
rename : build/tests/unit-ConfigStatus.py => python/mozbuild/mozbuild/test/backend/test_configenvironment.py
This commit is contained in:
Gregory Szorc 2013-01-29 06:24:24 -08:00
parent 04f89eb353
commit bddcff9d72
10 changed files with 305 additions and 459 deletions

View File

@ -6,15 +6,13 @@
# drop-in replacement for autoconf 2.13's config.status, with features
# borrowed from autoconf > 2.5, and additional features.
from __future__ import with_statement
from optparse import OptionParser
import sys, re, os, posixpath, ntpath
import errno
from StringIO import StringIO
from os.path import relpath
import os
import sys
from optparse import OptionParser
from mozbuild.backend.configenvironment import ConfigEnvironment
# Standalone js doesn't have virtualenv.
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'config'))
from Preprocessor import Preprocessor
# Basic logging facility
@ -24,200 +22,6 @@ def log(string):
print >>sys.stderr, string
def ensureParentDir(file):
'''Ensures the directory parent to the given file exists'''
dir = os.path.dirname(file)
if dir and not os.path.exists(dir):
try:
os.makedirs(dir)
except OSError, error:
if error.errno != errno.EEXIST:
raise
class FileAvoidWrite(StringIO):
'''file-like object that buffers its output and only writes it to disk
if the new contents are different from what the file may already contain.
'''
def __init__(self, filename):
self.filename = filename
StringIO.__init__(self)
def close(self):
buf = self.getvalue()
StringIO.close(self)
try:
file = open(self.filename, 'rU')
except IOError:
pass
else:
try:
if file.read() == buf:
log("%s is unchanged" % relpath(self.filename, os.curdir))
return
except IOError:
pass
finally:
file.close()
log("creating %s" % relpath(self.filename, os.curdir))
ensureParentDir(self.filename)
with open(self.filename, 'w') as file:
file.write(buf)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
def shell_escape(s):
'''Escape some characters with a backslash, and double dollar signs.
'''
return re.sub('''([ \t`#$^&*(){}\\|;'"<>?\[\]])''', r'\\\1', str(s)).replace('$', '$$')
class ConfigEnvironment(object):
'''A ConfigEnvironment is defined by a source directory and a build
directory. It preprocesses files from the source directory and stores
the result in the object directory.
There are two types of files: config files and config headers,
each treated through a different member function.
Creating a ConfigEnvironment requires a few arguments:
- topsrcdir and topobjdir are, respectively, the top source and
the top object directory.
- defines is a list of (name, value) tuples. In autoconf, these are
set with AC_DEFINE and AC_DEFINE_UNQUOTED
- non_global_defines are a list of names appearing in defines above
that are not meant to be exported in ACDEFINES and ALLDEFINES (see
below)
- substs is a list of (name, value) tuples. In autoconf, these are
set with AC_SUBST.
ConfigEnvironment automatically defines two additional substs variables
from all the defines not appearing in non_global_defines:
- ACDEFINES contains the defines in the form -DNAME=VALUE, for use on
preprocessor command lines. The order in which defines were given
when creating the ConfigEnvironment is preserved.
- ALLDEFINES contains the defines in the form #define NAME VALUE, in
sorted order, for use in config files, for an automatic listing of
defines.
and another additional subst variable from all the other substs:
- ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted
order, for use in autoconf.mk. It includes ACDEFINES, but doesn't
include ALLDEFINES.
ConfigEnvironment expects a "top_srcdir" subst to be set with the top
source directory, in msys format on windows. It is used to derive a
"srcdir" subst when treating config files. It can either be an absolute
path or a path relative to the topobjdir.
'''
def __init__(self, topobjdir = '.', topsrcdir = '.',
defines = [], non_global_defines = [], substs = []):
self.defines = dict(defines)
self.substs = dict(substs)
self.topsrcdir = topsrcdir
self.topobjdir = topobjdir
global_defines = [name for name, value in defines if not name in non_global_defines]
self.substs['ACDEFINES'] = ' '.join(["-D%s=%s" % (name, shell_escape(self.defines[name])) for name in global_defines])
self.substs['ALLSUBSTS'] = '\n'.join(sorted(["%s = %s" % (name, self.substs[name]) for name in self.substs]))
self.substs['ALLDEFINES'] = '\n'.join(sorted(["#define %s %s" % (name, self.defines[name]) for name in global_defines]))
def get_relative_srcdir(self, file):
'''Returns the relative source directory for the given file, always
using / as a path separator.
'''
assert(isinstance(file, basestring))
dir = posixpath.dirname(relpath(file, self.topobjdir).replace(os.sep, '/'))
if dir:
return dir
return '.'
def get_top_srcdir(self, file):
'''Returns a normalized top_srcdir for the given file: if
substs['top_srcdir'] is a relative path, it is relative to the
topobjdir. Adjust it to be relative to the file path.'''
top_srcdir = self.substs['top_srcdir']
if posixpath.isabs(top_srcdir) or ntpath.isabs(top_srcdir):
return top_srcdir
return posixpath.normpath(posixpath.join(self.get_depth(file), top_srcdir))
def get_file_srcdir(self, file):
'''Returns the srcdir for the given file, where srcdir is in msys
format on windows, thus derived from top_srcdir.
'''
dir = self.get_relative_srcdir(file)
top_srcdir = self.get_top_srcdir(file)
return posixpath.normpath(posixpath.join(top_srcdir, dir))
def get_depth(self, file):
'''Returns the DEPTH for the given file, that is, the path to the
object directory relative to the directory containing the given file.
Always uses / as a path separator.
'''
return relpath(self.topobjdir, os.path.dirname(file)).replace(os.sep, '/')
def get_input(self, file):
'''Returns the input file path in the source tree that can be used
to create the given config file or header.
'''
assert(isinstance(file, basestring))
return os.path.normpath(os.path.join(self.topsrcdir, "%s.in" % relpath(file, self.topobjdir)))
def create_config_file(self, path):
'''Creates the given config file. A config file is generated by
taking the corresponding source file and replacing occurences of
"@VAR@" by the value corresponding to "VAR" in the substs dict.
Additional substs are defined according to the file being treated:
"srcdir" for its the path to its source directory
"relativesrcdir" for its source directory relative to the top
"DEPTH" for the path to the top object directory
'''
input = self.get_input(path)
pp = Preprocessor()
pp.context.update(self.substs)
pp.context.update(top_srcdir = self.get_top_srcdir(path))
pp.context.update(srcdir = self.get_file_srcdir(path))
pp.context.update(relativesrcdir = self.get_relative_srcdir(path))
pp.context.update(DEPTH = self.get_depth(path))
pp.do_filter('attemptSubstitution')
pp.setMarker(None)
with FileAvoidWrite(path) as pp.out:
pp.do_include(input)
def create_config_header(self, path):
'''Creates the given config header. A config header is generated by
taking the corresponding source file and replacing some #define/#undef
occurences:
"#undef NAME" is turned into "#define NAME VALUE"
"#define NAME" is unchanged
"#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE"
"#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */"
Whitespaces are preserved.
'''
with open(self.get_input(path), 'rU') as input:
ensureParentDir(path)
output = FileAvoidWrite(path)
r = re.compile('^\s*#\s*(?P<cmd>[a-z]+)(?:\s+(?P<name>\S+)(?:\s+(?P<value>\S+))?)?', re.U)
for l in input:
m = r.match(l)
if m:
cmd = m.group('cmd')
name = m.group('name')
value = m.group('value')
if name:
if name in self.defines:
if cmd == 'define' and value:
l = l[:m.start('value')] + str(self.defines[name]) + l[m.end('value'):]
elif cmd == 'undef':
l = l[:m.start('cmd')] + 'define' + l[m.end('cmd'):m.end('name')] + ' ' + str(self.defines[name]) + l[m.end('name'):]
elif cmd == 'undef':
l = '/* ' + l[:m.end('name')] + ' */' + l[m.end('name'):]
output.write(l)
output.close()
def config_status(topobjdir = '.', topsrcdir = '.',
defines = [], non_global_defines = [], substs = [],
files = [], headers = []):
@ -276,9 +80,8 @@ def config_status(topobjdir = '.', topsrcdir = '.',
if not options.not_topobjdir:
topobjdir = '.'
env = ConfigEnvironment(topobjdir = topobjdir, topsrcdir = topsrcdir,
defines = defines, non_global_defines = non_global_defines,
substs = substs)
env = ConfigEnvironment(topsrcdir, topobjdir, defines=defines,
non_global_defines=non_global_defines, substs=substs)
if options.recheck:
# Execute configure from the top object directory

View File

@ -103,7 +103,6 @@ GDBINIT_DEST = $(FINAL_TARGET)
INSTALL_TARGETS += GDBINIT
PYTHON_UNIT_TESTS := \
tests/unit-ConfigStatus.py \
tests/test.py \
$(NULL)

View File

@ -6,15 +6,13 @@
# drop-in replacement for autoconf 2.13's config.status, with features
# borrowed from autoconf > 2.5, and additional features.
from __future__ import with_statement
from optparse import OptionParser
import sys, re, os, posixpath, ntpath
import errno
from StringIO import StringIO
from os.path import relpath
import os
import sys
from optparse import OptionParser
from mozbuild.backend.configenvironment import ConfigEnvironment
# Standalone js doesn't have virtualenv.
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'config'))
from Preprocessor import Preprocessor
# Basic logging facility
@ -24,200 +22,6 @@ def log(string):
print >>sys.stderr, string
def ensureParentDir(file):
'''Ensures the directory parent to the given file exists'''
dir = os.path.dirname(file)
if dir and not os.path.exists(dir):
try:
os.makedirs(dir)
except OSError, error:
if error.errno != errno.EEXIST:
raise
class FileAvoidWrite(StringIO):
'''file-like object that buffers its output and only writes it to disk
if the new contents are different from what the file may already contain.
'''
def __init__(self, filename):
self.filename = filename
StringIO.__init__(self)
def close(self):
buf = self.getvalue()
StringIO.close(self)
try:
file = open(self.filename, 'rU')
except IOError:
pass
else:
try:
if file.read() == buf:
log("%s is unchanged" % relpath(self.filename, os.curdir))
return
except IOError:
pass
finally:
file.close()
log("creating %s" % relpath(self.filename, os.curdir))
ensureParentDir(self.filename)
with open(self.filename, 'w') as file:
file.write(buf)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
def shell_escape(s):
'''Escape some characters with a backslash, and double dollar signs.
'''
return re.sub('''([ \t`#$^&*(){}\\|;'"<>?\[\]])''', r'\\\1', str(s)).replace('$', '$$')
class ConfigEnvironment(object):
'''A ConfigEnvironment is defined by a source directory and a build
directory. It preprocesses files from the source directory and stores
the result in the object directory.
There are two types of files: config files and config headers,
each treated through a different member function.
Creating a ConfigEnvironment requires a few arguments:
- topsrcdir and topobjdir are, respectively, the top source and
the top object directory.
- defines is a list of (name, value) tuples. In autoconf, these are
set with AC_DEFINE and AC_DEFINE_UNQUOTED
- non_global_defines are a list of names appearing in defines above
that are not meant to be exported in ACDEFINES and ALLDEFINES (see
below)
- substs is a list of (name, value) tuples. In autoconf, these are
set with AC_SUBST.
ConfigEnvironment automatically defines two additional substs variables
from all the defines not appearing in non_global_defines:
- ACDEFINES contains the defines in the form -DNAME=VALUE, for use on
preprocessor command lines. The order in which defines were given
when creating the ConfigEnvironment is preserved.
- ALLDEFINES contains the defines in the form #define NAME VALUE, in
sorted order, for use in config files, for an automatic listing of
defines.
and another additional subst variable from all the other substs:
- ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted
order, for use in autoconf.mk. It includes ACDEFINES, but doesn't
include ALLDEFINES.
ConfigEnvironment expects a "top_srcdir" subst to be set with the top
source directory, in msys format on windows. It is used to derive a
"srcdir" subst when treating config files. It can either be an absolute
path or a path relative to the topobjdir.
'''
def __init__(self, topobjdir = '.', topsrcdir = '.',
defines = [], non_global_defines = [], substs = []):
self.defines = dict(defines)
self.substs = dict(substs)
self.topsrcdir = topsrcdir
self.topobjdir = topobjdir
global_defines = [name for name, value in defines if not name in non_global_defines]
self.substs['ACDEFINES'] = ' '.join(["-D%s=%s" % (name, shell_escape(self.defines[name])) for name in global_defines])
self.substs['ALLSUBSTS'] = '\n'.join(sorted(["%s = %s" % (name, self.substs[name]) for name in self.substs]))
self.substs['ALLDEFINES'] = '\n'.join(sorted(["#define %s %s" % (name, self.defines[name]) for name in global_defines]))
def get_relative_srcdir(self, file):
'''Returns the relative source directory for the given file, always
using / as a path separator.
'''
assert(isinstance(file, basestring))
dir = posixpath.dirname(relpath(file, self.topobjdir).replace(os.sep, '/'))
if dir:
return dir
return '.'
def get_top_srcdir(self, file):
'''Returns a normalized top_srcdir for the given file: if
substs['top_srcdir'] is a relative path, it is relative to the
topobjdir. Adjust it to be relative to the file path.'''
top_srcdir = self.substs['top_srcdir']
if posixpath.isabs(top_srcdir) or ntpath.isabs(top_srcdir):
return top_srcdir
return posixpath.normpath(posixpath.join(self.get_depth(file), top_srcdir))
def get_file_srcdir(self, file):
'''Returns the srcdir for the given file, where srcdir is in msys
format on windows, thus derived from top_srcdir.
'''
dir = self.get_relative_srcdir(file)
top_srcdir = self.get_top_srcdir(file)
return posixpath.normpath(posixpath.join(top_srcdir, dir))
def get_depth(self, file):
'''Returns the DEPTH for the given file, that is, the path to the
object directory relative to the directory containing the given file.
Always uses / as a path separator.
'''
return relpath(self.topobjdir, os.path.dirname(file)).replace(os.sep, '/')
def get_input(self, file):
'''Returns the input file path in the source tree that can be used
to create the given config file or header.
'''
assert(isinstance(file, basestring))
return os.path.normpath(os.path.join(self.topsrcdir, "%s.in" % relpath(file, self.topobjdir)))
def create_config_file(self, path):
'''Creates the given config file. A config file is generated by
taking the corresponding source file and replacing occurences of
"@VAR@" by the value corresponding to "VAR" in the substs dict.
Additional substs are defined according to the file being treated:
"srcdir" for its the path to its source directory
"relativesrcdir" for its source directory relative to the top
"DEPTH" for the path to the top object directory
'''
input = self.get_input(path)
pp = Preprocessor()
pp.context.update(self.substs)
pp.context.update(top_srcdir = self.get_top_srcdir(path))
pp.context.update(srcdir = self.get_file_srcdir(path))
pp.context.update(relativesrcdir = self.get_relative_srcdir(path))
pp.context.update(DEPTH = self.get_depth(path))
pp.do_filter('attemptSubstitution')
pp.setMarker(None)
with FileAvoidWrite(path) as pp.out:
pp.do_include(input)
def create_config_header(self, path):
'''Creates the given config header. A config header is generated by
taking the corresponding source file and replacing some #define/#undef
occurences:
"#undef NAME" is turned into "#define NAME VALUE"
"#define NAME" is unchanged
"#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE"
"#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */"
Whitespaces are preserved.
'''
with open(self.get_input(path), 'rU') as input:
ensureParentDir(path)
output = FileAvoidWrite(path)
r = re.compile('^\s*#\s*(?P<cmd>[a-z]+)(?:\s+(?P<name>\S+)(?:\s+(?P<value>\S+))?)?', re.U)
for l in input:
m = r.match(l)
if m:
cmd = m.group('cmd')
name = m.group('name')
value = m.group('value')
if name:
if name in self.defines:
if cmd == 'define' and value:
l = l[:m.start('value')] + str(self.defines[name]) + l[m.end('value'):]
elif cmd == 'undef':
l = l[:m.start('cmd')] + 'define' + l[m.end('cmd'):m.end('name')] + ' ' + str(self.defines[name]) + l[m.end('name'):]
elif cmd == 'undef':
l = '/* ' + l[:m.end('name')] + ' */' + l[m.end('name'):]
output.write(l)
output.close()
def config_status(topobjdir = '.', topsrcdir = '.',
defines = [], non_global_defines = [], substs = [],
files = [], headers = []):
@ -276,9 +80,8 @@ def config_status(topobjdir = '.', topsrcdir = '.',
if not options.not_topobjdir:
topobjdir = '.'
env = ConfigEnvironment(topobjdir = topobjdir, topsrcdir = topsrcdir,
defines = defines, non_global_defines = non_global_defines,
substs = substs)
env = ConfigEnvironment(topsrcdir, topobjdir, defines=defines,
non_global_defines=non_global_defines, substs=substs)
if options.recheck:
# Execute configure from the top object directory

View File

@ -11,6 +11,7 @@ include $(DEPTH)/config/autoconf.mk
test_dirs := \
mozbuild/mozbuild/test \
mozbuild/mozbuild/test/backend \
mozbuild/mozbuild/test/compilation \
mozbuild/mozbuild/test/frontend \
mozbuild/mozpack/test \

View File

@ -0,0 +1,179 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import unicode_literals
import ntpath
import os
import posixpath
import re
from os.path import relpath
from Preprocessor import Preprocessor
from ..util import (
ensureParentDir,
FileAvoidWrite,
)
RE_SHELL_ESCAPE = re.compile('''([ \t`#$^&*(){}\\|;'"<>?\[\]])''')
def shell_escape(s):
"""Escape some characters with a backslash, and double dollar signs."""
return RE_SHELL_ESCAPE.sub(r'\\\1', str(s)).replace('$', '$$')
class ConfigEnvironment(object):
"""Perform actions associated with a configured but bare objdir.
The purpose of this class is to preprocess files from the source directory
and output results in the object directory.
There are two types of files: config files and config headers,
each treated through a different member function.
Creating a ConfigEnvironment requires a few arguments:
- topsrcdir and topobjdir are, respectively, the top source and
the top object directory.
- defines is a list of (name, value) tuples. In autoconf, these are
set with AC_DEFINE and AC_DEFINE_UNQUOTED
- non_global_defines are a list of names appearing in defines above
that are not meant to be exported in ACDEFINES and ALLDEFINES (see
below)
- substs is a list of (name, value) tuples. In autoconf, these are
set with AC_SUBST.
ConfigEnvironment automatically defines two additional substs variables
from all the defines not appearing in non_global_defines:
- ACDEFINES contains the defines in the form -DNAME=VALUE, for use on
preprocessor command lines. The order in which defines were given
when creating the ConfigEnvironment is preserved.
- ALLDEFINES contains the defines in the form #define NAME VALUE, in
sorted order, for use in config files, for an automatic listing of
defines.
and another additional subst variable from all the other substs:
- ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted
order, for use in autoconf.mk. It includes ACDEFINES, but doesn't
include ALLDEFINES.
ConfigEnvironment expects a "top_srcdir" subst to be set with the top
source directory, in msys format on windows. It is used to derive a
"srcdir" subst when treating config files. It can either be an absolute
path or a path relative to the topobjdir.
"""
def __init__(self, topsrcdir, topobjdir, defines=[], non_global_defines=[],
substs=[]):
self.defines = dict(defines)
self.substs = dict(substs)
self.topsrcdir = topsrcdir
self.topobjdir = topobjdir
global_defines = [name for name, value in defines
if not name in non_global_defines]
self.substs['ACDEFINES'] = ' '.join(['-D%s=%s' % (name,
shell_escape(self.defines[name])) for name in global_defines])
self.substs['ALLSUBSTS'] = '\n'.join(sorted(['%s = %s' % (name,
self.substs[name]) for name in self.substs]))
self.substs['ALLDEFINES'] = '\n'.join(sorted(['#define %s %s' % (name,
self.defines[name]) for name in global_defines]))
def get_relative_srcdir(self, file):
'''Returns the relative source directory for the given file, always
using / as a path separator.
'''
assert(isinstance(file, basestring))
dir = posixpath.dirname(relpath(file, self.topobjdir).replace(os.sep, '/'))
if dir:
return dir
return '.'
def get_top_srcdir(self, file):
'''Returns a normalized top_srcdir for the given file: if
substs['top_srcdir'] is a relative path, it is relative to the
topobjdir. Adjust it to be relative to the file path.'''
top_srcdir = self.substs['top_srcdir']
if posixpath.isabs(top_srcdir) or ntpath.isabs(top_srcdir):
return top_srcdir
return posixpath.normpath(posixpath.join(self.get_depth(file), top_srcdir))
def get_file_srcdir(self, file):
'''Returns the srcdir for the given file, where srcdir is in msys
format on windows, thus derived from top_srcdir.
'''
dir = self.get_relative_srcdir(file)
top_srcdir = self.get_top_srcdir(file)
return posixpath.normpath(posixpath.join(top_srcdir, dir))
def get_depth(self, file):
'''Returns the DEPTH for the given file, that is, the path to the
object directory relative to the directory containing the given file.
Always uses / as a path separator.
'''
return relpath(self.topobjdir, os.path.dirname(file)).replace(os.sep, '/')
def get_input(self, file):
'''Returns the input file path in the source tree that can be used
to create the given config file or header.
'''
assert(isinstance(file, basestring))
return os.path.normpath(os.path.join(self.topsrcdir, "%s.in" % relpath(file, self.topobjdir)))
def create_config_file(self, path):
'''Creates the given config file. A config file is generated by
taking the corresponding source file and replacing occurences of
"@VAR@" by the value corresponding to "VAR" in the substs dict.
Additional substs are defined according to the file being treated:
"srcdir" for its the path to its source directory
"relativesrcdir" for its source directory relative to the top
"DEPTH" for the path to the top object directory
'''
input = self.get_input(path)
pp = Preprocessor()
pp.context.update(self.substs)
pp.context.update(top_srcdir = self.get_top_srcdir(path))
pp.context.update(srcdir = self.get_file_srcdir(path))
pp.context.update(relativesrcdir = self.get_relative_srcdir(path))
pp.context.update(DEPTH = self.get_depth(path))
pp.do_filter('attemptSubstitution')
pp.setMarker(None)
with FileAvoidWrite(path) as pp.out:
pp.do_include(input)
def create_config_header(self, path):
'''Creates the given config header. A config header is generated by
taking the corresponding source file and replacing some #define/#undef
occurences:
"#undef NAME" is turned into "#define NAME VALUE"
"#define NAME" is unchanged
"#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE"
"#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */"
Whitespaces are preserved.
'''
with open(self.get_input(path), 'rU') as input:
ensureParentDir(path)
output = FileAvoidWrite(path)
r = re.compile('^\s*#\s*(?P<cmd>[a-z]+)(?:\s+(?P<name>\S+)(?:\s+(?P<value>\S+))?)?', re.U)
for l in input:
m = r.match(l)
if m:
cmd = m.group('cmd')
name = m.group('name')
value = m.group('value')
if name:
if name in self.defines:
if cmd == 'define' and value:
l = l[:m.start('value')] + str(self.defines[name]) + l[m.end('value'):]
elif cmd == 'undef':
l = l[:m.start('cmd')] + 'define' + l[m.end('cmd'):m.end('name')] + ' ' + str(self.defines[name]) + l[m.end('name'):]
elif cmd == 'undef':
l = '/* ' + l[:m.end('name')] + ' */' + l[m.end('name'):]
output.write(l)
output.close()

View File

@ -1,55 +1,23 @@
from __future__ import with_statement
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os, posixpath
from StringIO import StringIO
import unittest
from mozunit import main, MockedOpen
import ConfigStatus
from ConfigStatus import FileAvoidWrite
import mozbuild.backend.configenvironment as ConfigStatus
class ConfigEnvironment(ConfigStatus.ConfigEnvironment):
def __init__(self, **args):
ConfigStatus.ConfigEnvironment.__init__(self, **args)
def __init__(self, *args, **kwargs):
ConfigStatus.ConfigEnvironment.__init__(self, *args, **kwargs)
# Be helpful to unit tests
if not 'top_srcdir' in self.substs:
if os.path.isabs(self.topsrcdir):
self.substs['top_srcdir'] = self.topsrcdir.replace(os.sep, '/')
else:
self.substs['top_srcdir'] = ConfigStatus.relpath(self.topsrcdir, self.topobjdir).replace(os.sep, '/')
class TestFileAvoidWrite(unittest.TestCase):
def test_file_avoid_write(self):
'''Test the FileAvoidWrite class
'''
with MockedOpen({'file': 'content'}):
# Overwriting an existing file replaces its content
with FileAvoidWrite('file') as file:
file.write('bazqux')
self.assertEqual(open('file', 'r').read(), 'bazqux')
# Creating a new file (obviously) stores its content
with FileAvoidWrite('file2') as file:
file.write('content')
self.assertEqual(open('file2').read(), 'content')
class MyMockedOpen(MockedOpen):
'''MockedOpen extension to raise an exception if something
attempts to write in an opened file.
'''
def __call__(self, name, mode):
if 'w' in mode:
raise Exception, 'Unexpected open with write mode'
return MockedOpen.__call__(self, name, mode)
with MyMockedOpen({'file': 'content'}):
# Validate that MyMockedOpen works as intended
file = FileAvoidWrite('file')
file.write('foobar')
self.assertRaises(Exception, file.close)
# Check that no write actually happens when writing the
# same content as what already is in the file
with FileAvoidWrite('file') as file:
file.write('content')
self.substs['top_srcdir'] = os.path.relpath(self.topsrcdir, self.topobjdir).replace(os.sep, '/')
class TestEnvironment(unittest.TestCase):
@ -57,7 +25,7 @@ class TestEnvironment(unittest.TestCase):
'''Test the automatically set values of ACDEFINES, ALLDEFINES
and ALLSUBSTS.
'''
env = ConfigEnvironment(
env = ConfigEnvironment('.', '.',
defines = [ ('foo', 'bar'), ('baz', 'qux 42'),
('abc', 'def'), ('extra', 'foobar') ],
non_global_defines = ['extra', 'ignore'],
@ -86,7 +54,7 @@ zzz = "abc def"''')
@foo@
@bar@
'''}):
env = ConfigEnvironment(substs = [ ('foo', 'bar baz') ])
env = ConfigEnvironment('.', '.', substs = [ ('foo', 'bar baz') ])
env.create_config_file('file')
self.assertEqual(open('file', 'r').read(), '''#ifdef foo
bar baz
@ -113,7 +81,7 @@ bar baz
# define foo 42
#endif
'''}):
env = ConfigEnvironment(defines = [ ('foo', 'baz qux'), ('baz', 1) ])
env = ConfigEnvironment('.', '.', defines = [ ('foo', 'baz qux'), ('baz', 1) ])
env.create_config_header('file')
self.assertEqual(open('file','r').read(), '''
/* Comment */

View File

@ -1,6 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import unicode_literals
@ -8,9 +8,15 @@ import hashlib
import unittest
from mozfile.mozfile import NamedTemporaryFile
from mozunit import main
from mozunit import (
main,
MockedOpen,
)
from mozbuild.util import hash_file
from mozbuild.util import (
FileAvoidWrite,
hash_file,
)
class TestHashing(unittest.TestCase):
@ -44,5 +50,39 @@ class TestHashing(unittest.TestCase):
self.assertEqual(actual, expected)
class TestFileAvoidWrite(unittest.TestCase):
def test_file_avoid_write(self):
with MockedOpen({'file': 'content'}):
# Overwriting an existing file replaces its content
with FileAvoidWrite('file') as file:
file.write('bazqux')
self.assertEqual(open('file', 'r').read(), 'bazqux')
# Creating a new file (obviously) stores its content
with FileAvoidWrite('file2') as file:
file.write('content')
self.assertEqual(open('file2').read(), 'content')
class MyMockedOpen(MockedOpen):
'''MockedOpen extension to raise an exception if something
attempts to write in an opened file.
'''
def __call__(self, name, mode):
if 'w' in mode:
raise Exception, 'Unexpected open with write mode'
return MockedOpen.__call__(self, name, mode)
with MyMockedOpen({'file': 'content'}):
# Validate that MyMockedOpen works as intended
file = FileAvoidWrite('file')
file.write('foobar')
self.assertRaises(Exception, file.close)
# Check that no write actually happens when writing the
# same content as what already is in the file
with FileAvoidWrite('file') as file:
file.write('content')
if __name__ == '__main__':
main()

View File

@ -8,7 +8,11 @@
from __future__ import unicode_literals
import copy
import errno
import hashlib
import os
from StringIO import StringIO
def hash_file(path):
@ -84,3 +88,52 @@ class ReadOnlyDefaultDict(DefaultOnReadDict, ReadOnlyDict):
def __init__(self, d, defaults=None, global_default=undefined):
DefaultOnReadDict.__init__(self, d, defaults, global_default)
def ensureParentDir(path):
"""Ensures the directory parent to the given file exists."""
d = os.path.dirname(path)
if d and not os.path.exists(path):
try:
os.makedirs(d)
except OSError, error:
if error.errno != errno.EEXIST:
raise
class FileAvoidWrite(StringIO):
"""File-like object that buffers output and only writes if content changed.
We create an instance from an existing filename. New content is written to
it. When we close the file object, if the content in the in-memory buffer
differs from what is on disk, then we write out the new content. Otherwise,
the original file is untouched.
"""
def __init__(self, filename):
StringIO.__init__(self)
self.filename = filename
def close(self):
buf = self.getvalue()
StringIO.close(self)
try:
existing = open(self.filename, 'rU')
except IOError:
pass
else:
try:
if existing.read() == buf:
return
except IOError:
pass
finally:
existing.close()
ensureParentDir(self.filename)
with open(self.filename, 'w') as file:
file.write(buf)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()